mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 05:30:35 +00:00
Split direct Proxmox settings surface owners
This commit is contained in:
parent
dcd88e2f64
commit
274f109dff
11 changed files with 584 additions and 338 deletions
|
|
@ -34,16 +34,26 @@ management, and fleet control surfaces.
|
|||
10. `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`
|
||||
11. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`
|
||||
12. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`
|
||||
13. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
|
||||
14. `frontend-modern/src/components/Settings/UnifiedAgents.tsx`
|
||||
15. `frontend-modern/src/components/Settings/NodeModal.tsx`
|
||||
16. `frontend-modern/src/components/Settings/nodeModalModel.ts`
|
||||
17. `frontend-modern/src/components/Settings/useNodeModalState.ts`
|
||||
18. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
|
||||
19. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`
|
||||
20. `frontend-modern/src/utils/agentProfilesPresentation.ts`
|
||||
21. `frontend-modern/src/utils/agentInstallCommand.ts`
|
||||
22. `frontend-modern/src/api/nodes.ts`
|
||||
13. `frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx`
|
||||
14. `frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx`
|
||||
15. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
|
||||
16. `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`
|
||||
17. `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`
|
||||
18. `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`
|
||||
19. `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`
|
||||
20. `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`
|
||||
21. `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`
|
||||
22. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
|
||||
23. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`
|
||||
24. `frontend-modern/src/components/Settings/UnifiedAgents.tsx`
|
||||
25. `frontend-modern/src/components/Settings/NodeModal.tsx`
|
||||
26. `frontend-modern/src/components/Settings/nodeModalModel.ts`
|
||||
27. `frontend-modern/src/components/Settings/useNodeModalState.ts`
|
||||
28. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
|
||||
29. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`
|
||||
30. `frontend-modern/src/utils/agentProfilesPresentation.ts`
|
||||
31. `frontend-modern/src/utils/agentInstallCommand.ts`
|
||||
32. `frontend-modern/src/api/nodes.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -70,7 +80,7 @@ management, and fleet control surfaces.
|
|||
3. Add or change runtime-side Unified Agent startup, first-report assembly, and enroll/runtime continuity through `internal/hostagent/`.
|
||||
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 installer flags, persisted service arguments, or upgrade-safe re-entry behavior through `scripts/install.sh` and `scripts/install.ps1`.
|
||||
6. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, shared frontend install-command assembly, Proxmox setup/install API transport, 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/useInfrastructureOperationsState.tsx`, `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`, `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`.
|
||||
6. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, the direct Proxmox workspace shell and section owners, shared frontend install-command assembly, Proxmox setup/install API transport, 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/InfrastructureReportingPanel.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.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/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.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`, `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
@ -137,6 +147,15 @@ reintroducing local array or JSON parsing rules.
|
|||
The owned backend API surfaces must preserve the exact-release installer
|
||||
fallback, canonical /api/auto-register behavior, and hosted org install-command
|
||||
contracts instead of leaving those guarantees implied by generic API ownership.
|
||||
The direct Proxmox fallback workspace now also lives explicitly inside this
|
||||
lifecycle boundary: `InfrastructureWorkspace.tsx`,
|
||||
`InfrastructureInstallPanel.tsx`, `InfrastructureReportingPanel.tsx`,
|
||||
`ProxmoxSettingsPanel.tsx`, `ProxmoxDirectConnectionsCard.tsx`,
|
||||
`ProxmoxDiscoveryResultsCard.tsx`, `ProxmoxDeleteNodeDialog.tsx`,
|
||||
`ConfiguredNodeTables.tsx`, `SettingsSectionNav.tsx`, and
|
||||
`useInfrastructureSettingsState.ts` own the fallback install/direct/reporting
|
||||
operator flow instead of leaving those panels ungoverned beside the canonical
|
||||
unified-agent install path.
|
||||
That canonical /api/auto-register behavior now also includes hostname/IP continuity:
|
||||
reruns that arrive through a different canonical host form must reuse the same
|
||||
Pulse-managed node record and token instead of forking duplicate fleet entries.
|
||||
|
|
|
|||
|
|
@ -368,13 +368,23 @@
|
|||
"frontend-modern/src/api/nodes.ts",
|
||||
"frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx",
|
||||
"frontend-modern/src/components/Settings/AgentProfilesPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx",
|
||||
"frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
|
||||
"frontend-modern/src/components/Settings/NodeModal.tsx",
|
||||
"frontend-modern/src/components/Settings/nodeModalModel.ts",
|
||||
"frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/SettingsSectionNav.tsx",
|
||||
"frontend-modern/src/components/Settings/UnifiedAgents.tsx",
|
||||
"frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts",
|
||||
"frontend-modern/src/components/Settings/useNodeModalState.ts",
|
||||
"frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx",
|
||||
"frontend-modern/src/utils/agentInstallCommand.ts",
|
||||
|
|
@ -587,6 +597,32 @@
|
|||
"frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx",
|
||||
"frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "direct-proxmox-workspace-surface",
|
||||
"label": "direct Proxmox workspace lifecycle proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx",
|
||||
"frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/SettingsSectionNav.tsx",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx",
|
||||
"frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts",
|
||||
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
|
||||
"frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx",
|
||||
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { Show } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
|
||||
interface ProxmoxDeleteNodeDialogProps {
|
||||
deleteNodeLoading: boolean;
|
||||
nodePendingDeleteHost: string;
|
||||
nodePendingDeleteLabel: string;
|
||||
nodePendingDeleteType: string;
|
||||
nodePendingDeleteTypeLabel: string;
|
||||
onCancel: () => void;
|
||||
onDelete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const ProxmoxDeleteNodeDialog: Component<ProxmoxDeleteNodeDialogProps> = (props) => {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
onClose={props.onCancel}
|
||||
panelClass="max-w-lg"
|
||||
closeOnBackdrop={false}
|
||||
ariaLabel={`Remove ${props.nodePendingDeleteLabel}`}
|
||||
>
|
||||
<Card padding="lg" class="w-full max-w-lg space-y-5">
|
||||
<SectionHeader title={`Remove ${props.nodePendingDeleteLabel}`} size="md" class="mb-1" />
|
||||
<div class="space-y-3 text-sm text-gray-600">
|
||||
<p>
|
||||
Removing this {props.nodePendingDeleteTypeLabel.toLowerCase()} also scrubs the Pulse
|
||||
footprint on the host - the proxy service, SSH key, API token, and bind mount are all
|
||||
cleaned up automatically.
|
||||
</p>
|
||||
<div class="rounded-md border border-blue-200 bg-blue-50 p-3 text-sm leading-relaxed dark:border-blue-800 dark:bg-blue-900 dark:text-blue-100">
|
||||
<p class="font-medium text-blue-900 dark:text-blue-100">What happens next</p>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-4 text-blue-800 dark:text-blue-200 text-sm">
|
||||
<li>Pulse removes the node entry and clears related alerts.</li>
|
||||
<li>
|
||||
{props.nodePendingDeleteHost ? (
|
||||
<>
|
||||
The host <span class="font-semibold">{props.nodePendingDeleteHost}</span>{' '}
|
||||
loses the proxy service, SSH key, and API token.
|
||||
</>
|
||||
) : (
|
||||
'The host loses the proxy service, SSH key, and API token.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
If the host comes back later, rerunning the setup script reinstalls everything with
|
||||
a fresh key.
|
||||
</li>
|
||||
<Show when={props.nodePendingDeleteType === 'pbs'}>
|
||||
<li>
|
||||
Backup user tokens on the PBS are removed, so jobs referencing them will no
|
||||
longer authenticate until the node is re-added.
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={props.nodePendingDeleteType === 'pmg'}>
|
||||
<li>
|
||||
Mail gateway tokens are removed as part of the cleanup; re-enroll to restore
|
||||
outbound telemetry.
|
||||
</li>
|
||||
</Show>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onCancel}
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
disabled={props.deleteNodeLoading}
|
||||
>
|
||||
Keep node
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onDelete}
|
||||
disabled={props.deleteNodeLoading}
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-red-500 dark:hover:bg-red-400"
|
||||
>
|
||||
{props.deleteNodeLoading ? 'Removing…' : 'Remove node'}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import type { Component, JSX } from 'solid-js';
|
||||
import { Dynamic } from 'solid-js/web';
|
||||
import { Show } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import type { ToggleChangeEvent } from '@/components/shared/Toggle';
|
||||
import { getSettingsConfigurationLoadingState } from '@/utils/settingsShellPresentation';
|
||||
import type { ProxmoxVariantPresentation } from '@/utils/proxmoxSettingsPresentation';
|
||||
import type { NodeConfigWithStatus } from '@/types/nodes';
|
||||
import type { DiscoveredServer, NodeType } from './useInfrastructureSettingsState';
|
||||
|
||||
interface ProxmoxDirectConnectionsCardProps {
|
||||
activeAgent: NodeType;
|
||||
activeConfig: ProxmoxVariantPresentation;
|
||||
activeConfiguredNodes: NodeConfigWithStatus[];
|
||||
activeDiscoveredNodes: DiscoveredServer[];
|
||||
configuredTable: JSX.Element;
|
||||
discoveryEnabled: boolean;
|
||||
envOverrides: Record<string, boolean>;
|
||||
initialLoadComplete: boolean;
|
||||
onDiscoveryToggle: (event: ToggleChangeEvent) => Promise<void>;
|
||||
onOpenCreateNode: (type: NodeType) => void;
|
||||
onRefreshDiscovery: () => Promise<void>;
|
||||
savingDiscoverySettings: boolean;
|
||||
}
|
||||
|
||||
export const ProxmoxDirectConnectionsCard: Component<ProxmoxDirectConnectionsCardProps> = (
|
||||
props,
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<Show when={!props.initialLoadComplete}>
|
||||
<div class="flex items-center justify-center rounded-md border border-dashed border-border bg-surface-alt py-12 text-sm text-muted">
|
||||
{getSettingsConfigurationLoadingState().text}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.initialLoadComplete}>
|
||||
<Card padding="none" class="rounded-xl border border-border shadow-sm">
|
||||
<div class="px-3 py-4 sm:px-6 sm:py-6 space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-base font-semibold text-base-content">{props.activeConfig.title}</h4>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center rounded-full bg-surface-alt px-2.5 py-1 font-medium text-base-content">
|
||||
{props.activeConfiguredNodes.length} configured
|
||||
</span>
|
||||
<Show when={props.discoveryEnabled}>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-50 px-2.5 py-1 font-medium text-blue-700 dark:bg-blue-950/40 dark:text-blue-300">
|
||||
{props.activeDiscoveredNodes.length} discovered
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-start gap-2 sm:justify-end">
|
||||
<div
|
||||
class="flex items-center gap-2 sm:gap-3"
|
||||
title="Enable automatic discovery of Proxmox servers on your network"
|
||||
>
|
||||
<span class="text-xs sm:text-sm text-muted">Discovery</span>
|
||||
<Toggle
|
||||
checked={props.discoveryEnabled}
|
||||
onChange={props.onDiscoveryToggle}
|
||||
disabled={
|
||||
props.envOverrides.discoveryEnabled || props.savingDiscoverySettings
|
||||
}
|
||||
containerClass="gap-2"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{props.discoveryEnabled ? 'On' : 'Off'}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={props.discoveryEnabled}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onRefreshDiscovery}
|
||||
class="px-2 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors flex items-center gap-1"
|
||||
title="Refresh discovered servers"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<polyline points="1 20 1 14 7 14"></polyline>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Refresh</span>
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onOpenCreateNode(props.activeAgent)}
|
||||
class="px-2 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span class="sm:hidden">Add</span>
|
||||
<span class="hidden sm:inline">{props.activeConfig.addLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.activeConfiguredNodes.length > 0}>{props.configuredTable}</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.activeConfiguredNodes.length === 0 &&
|
||||
props.activeDiscoveredNodes.length === 0
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||
<div class="rounded-full bg-surface-alt p-4 mb-4">
|
||||
<Dynamic component={props.activeConfig.emptyIcon} class="h-8 w-8 text-muted" />
|
||||
</div>
|
||||
<p class="text-base font-medium text-base-content mb-1">
|
||||
{props.activeConfig.emptyTitle}
|
||||
</p>
|
||||
<p class="text-sm text-muted">{props.activeConfig.emptyDescription}</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { For, Show } from 'solid-js';
|
||||
import Loader from 'lucide-solid/icons/loader';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import type { ProxmoxVariantPresentation } from '@/utils/proxmoxSettingsPresentation';
|
||||
import type { DiscoveryScanStatus, DiscoveredServer } from './useInfrastructureSettingsState';
|
||||
|
||||
interface ProxmoxDiscoveryResultsCardProps {
|
||||
activeConfig: ProxmoxVariantPresentation;
|
||||
activeDiscoveredNodes: DiscoveredServer[];
|
||||
discoveryScanStatus: DiscoveryScanStatus;
|
||||
hasDiscoveryTimeouts: boolean;
|
||||
onOpenDiscoveredNode: (server: DiscoveredServer) => void;
|
||||
}
|
||||
|
||||
export const ProxmoxDiscoveryResultsCard: Component<ProxmoxDiscoveryResultsCardProps> = (
|
||||
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>
|
||||
<h4 class="text-base font-semibold text-base-content">Discovery results</h4>
|
||||
<p class="text-sm text-muted">
|
||||
Found servers that can be turned into direct connections for this Proxmox type.
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-xs text-muted">
|
||||
<Show when={props.discoveryScanStatus.scanning}>
|
||||
<span class="flex items-center gap-2">
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
<span>{props.activeConfig.scanningLabel}</span>
|
||||
</span>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
!props.discoveryScanStatus.scanning &&
|
||||
(props.discoveryScanStatus.lastResultAt ||
|
||||
props.discoveryScanStatus.lastScanStartedAt)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Last scan{' '}
|
||||
{formatRelativeTime(
|
||||
props.discoveryScanStatus.lastResultAt ??
|
||||
props.discoveryScanStatus.lastScanStartedAt,
|
||||
{ compact: true },
|
||||
)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.discoveryScanStatus.errors && props.discoveryScanStatus.errors.length > 0
|
||||
}
|
||||
>
|
||||
<div class="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900 border border-amber-200 dark:border-amber-800 rounded-md p-2">
|
||||
<span class="font-medium">Discovery issues:</span>
|
||||
<ul class="list-disc ml-4 mt-1 space-y-0.5">
|
||||
<For each={props.discoveryScanStatus.errors ?? []}>{(error) => <li>{error}</li>}</For>
|
||||
</ul>
|
||||
<Show when={props.hasDiscoveryTimeouts}>
|
||||
<p class="mt-2 text-[0.7rem] font-medium text-amber-700 dark:text-amber-300">
|
||||
Large networks can time out in auto mode. Switch to a custom subnet for faster,
|
||||
targeted scans.
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.discoveryScanStatus.scanning && props.activeDiscoveredNodes.length === 0}>
|
||||
<div class="flex items-center gap-2 text-xs text-muted">
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
<span>
|
||||
Waiting for responses… this can take up to a minute depending on your network size.
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={props.activeDiscoveredNodes.length > 0}
|
||||
fallback={
|
||||
<div class="rounded-md border border-dashed border-border px-4 py-6 text-sm text-muted">
|
||||
No discovery matches for this Proxmox type yet. You can still add a direct
|
||||
connection manually.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<For each={props.activeDiscoveredNodes}>
|
||||
{(server) => (
|
||||
<button
|
||||
type="button"
|
||||
class="w-full bg-surface-hover rounded-md p-4 border border-border text-left opacity-75 hover:opacity-100 hover:border-blue-300 dark:hover:border-blue-700 transition-all"
|
||||
onClick={() => props.onOpenDiscoveredNode(server)}
|
||||
>
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-3 h-3 mt-1.5 rounded-full bg-gray-400 animate-pulse"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="font-medium text-base-content">
|
||||
{props.activeConfig.titleFromServer(server)}
|
||||
</h4>
|
||||
<p class="text-sm mt-1">
|
||||
{server.ip}:{server.port}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-400 rounded">
|
||||
Discovered
|
||||
</span>
|
||||
<span class="text-xs text-muted">
|
||||
Click to configure this connection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" class="mt-1">
|
||||
<path
|
||||
d="M12 5v14m-7-7h14"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,27 +1,22 @@
|
|||
import { Component, Accessor, Setter, Show, For, createMemo, createSignal } from 'solid-js';
|
||||
import { Component, Accessor, Setter, Show, createMemo, createSignal } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import { Dynamic } from 'solid-js/web';
|
||||
import Loader from 'lucide-solid/icons/loader';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { PBSInstance, PMGInstance } from '@/types/api';
|
||||
import type { NodeConfig, NodeConfigWithStatus } from '@/types/nodes';
|
||||
import type { SecurityStatus as SecurityStatusInfo } from '@/types/config';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { CalloutCard } from '@/components/shared/CalloutCard';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import type { ToggleChangeEvent } from '@/components/shared/Toggle';
|
||||
import { NodeModal } from './NodeModal';
|
||||
import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables';
|
||||
import { ProxmoxDeleteNodeDialog } from './ProxmoxDeleteNodeDialog';
|
||||
import { ProxmoxDirectConnectionsCard } from './ProxmoxDirectConnectionsCard';
|
||||
import { ProxmoxDiscoveryResultsCard } from './ProxmoxDiscoveryResultsCard';
|
||||
import { SettingsSectionNav } from './SettingsSectionNav';
|
||||
import {
|
||||
buildProxmoxDiscoveryPrefillNode,
|
||||
getProxmoxVariantPresentation,
|
||||
} from '@/utils/proxmoxSettingsPresentation';
|
||||
import { getSettingsConfigurationLoadingState } from '@/utils/settingsShellPresentation';
|
||||
import type {
|
||||
DiscoveredServer,
|
||||
DiscoveryScanStatus,
|
||||
|
|
@ -265,329 +260,43 @@ export const ProxmoxSettingsPanel: Component<ProxmoxSettingsPanelProps> = (props
|
|||
|
||||
<div class="space-y-6 mt-6">
|
||||
<div class="space-y-4">
|
||||
<Show when={!props.initialLoadComplete()}>
|
||||
<div class="flex items-center justify-center rounded-md border border-dashed border-border bg-surface-alt py-12 text-sm text-muted">
|
||||
{getSettingsConfigurationLoadingState().text}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.initialLoadComplete()}>
|
||||
<Card padding="none" class="rounded-xl border border-border shadow-sm">
|
||||
<div class="px-3 py-4 sm:px-6 sm:py-6 space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-base font-semibold text-base-content">
|
||||
{activeConfig().title}
|
||||
</h4>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center rounded-full bg-surface-alt px-2.5 py-1 font-medium text-base-content">
|
||||
{activeConfiguredNodes().length} configured
|
||||
</span>
|
||||
<Show when={props.discoveryEnabled()}>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-50 px-2.5 py-1 font-medium text-blue-700 dark:bg-blue-950/40 dark:text-blue-300">
|
||||
{activeDiscoveredNodes().length} discovered
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-start gap-2 sm:justify-end">
|
||||
<div
|
||||
class="flex items-center gap-2 sm:gap-3"
|
||||
title="Enable automatic discovery of Proxmox servers on your network"
|
||||
>
|
||||
<span class="text-xs sm:text-sm text-muted">Discovery</span>
|
||||
<Toggle
|
||||
checked={props.discoveryEnabled()}
|
||||
onChange={handleDiscoveryToggle}
|
||||
disabled={
|
||||
props.envOverrides().discoveryEnabled || props.savingDiscoverySettings()
|
||||
}
|
||||
containerClass="gap-2"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{props.discoveryEnabled() ? 'On' : 'Off'}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={props.discoveryEnabled()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshDiscovery}
|
||||
class="px-2 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors flex items-center gap-1"
|
||||
title="Refresh discovered servers"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<polyline points="1 20 1 14 7 14"></polyline>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Refresh</span>
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateNode(activeAgent())}
|
||||
class="px-2 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span class="sm:hidden">Add</span>
|
||||
<span class="hidden sm:inline">{activeConfig().addLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={activeConfiguredNodes().length > 0}>{renderConfiguredTable()}</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
activeConfiguredNodes().length === 0 && activeDiscoveredNodes().length === 0
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||
<div class="rounded-full bg-surface-alt p-4 mb-4">
|
||||
<Dynamic component={activeConfig().emptyIcon} class="h-8 w-8 text-muted" />
|
||||
</div>
|
||||
<p class="text-base font-medium text-base-content mb-1">
|
||||
{activeConfig().emptyTitle}
|
||||
</p>
|
||||
<p class="text-sm text-muted">{activeConfig().emptyDescription}</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
</Show>
|
||||
<ProxmoxDirectConnectionsCard
|
||||
activeAgent={activeAgent()}
|
||||
activeConfig={activeConfig()}
|
||||
activeConfiguredNodes={activeConfiguredNodes()}
|
||||
activeDiscoveredNodes={activeDiscoveredNodes()}
|
||||
configuredTable={renderConfiguredTable()}
|
||||
discoveryEnabled={props.discoveryEnabled()}
|
||||
envOverrides={props.envOverrides()}
|
||||
initialLoadComplete={props.initialLoadComplete()}
|
||||
onDiscoveryToggle={handleDiscoveryToggle}
|
||||
onOpenCreateNode={openCreateNode}
|
||||
onRefreshDiscovery={handleRefreshDiscovery}
|
||||
savingDiscoverySettings={props.savingDiscoverySettings()}
|
||||
/>
|
||||
|
||||
<Show when={props.discoveryEnabled()}>
|
||||
<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>
|
||||
<h4 class="text-base font-semibold text-base-content">Discovery results</h4>
|
||||
<p class="text-sm text-muted">
|
||||
Found servers that can be turned into direct connections for this Proxmox
|
||||
type.
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-xs text-muted">
|
||||
<Show when={props.discoveryScanStatus().scanning}>
|
||||
<span class="flex items-center gap-2">
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
<span>{activeConfig().scanningLabel}</span>
|
||||
</span>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
!props.discoveryScanStatus().scanning &&
|
||||
(props.discoveryScanStatus().lastResultAt ||
|
||||
props.discoveryScanStatus().lastScanStartedAt)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Last scan{' '}
|
||||
{formatRelativeTime(
|
||||
props.discoveryScanStatus().lastResultAt ??
|
||||
props.discoveryScanStatus().lastScanStartedAt,
|
||||
{ compact: true },
|
||||
)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.discoveryScanStatus().errors && props.discoveryScanStatus().errors!.length
|
||||
}
|
||||
>
|
||||
<div class="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900 border border-amber-200 dark:border-amber-800 rounded-md p-2">
|
||||
<span class="font-medium">Discovery issues:</span>
|
||||
<ul class="list-disc ml-4 mt-1 space-y-0.5">
|
||||
<For each={props.discoveryScanStatus().errors || []}>
|
||||
{(error) => <li>{error}</li>}
|
||||
</For>
|
||||
</ul>
|
||||
<Show when={hasDiscoveryTimeouts()}>
|
||||
<p class="mt-2 text-[0.7rem] font-medium text-amber-700 dark:text-amber-300">
|
||||
Large networks can time out in auto mode. Switch to a custom subnet for
|
||||
faster, targeted scans.
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.discoveryScanStatus().scanning && activeDiscoveredNodes().length === 0
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-2 text-xs text-muted">
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
<span>
|
||||
Waiting for responses… this can take up to a minute depending on your network
|
||||
size.
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={activeDiscoveredNodes().length > 0}
|
||||
fallback={
|
||||
<div class="rounded-md border border-dashed border-border px-4 py-6 text-sm text-muted">
|
||||
No discovery matches for this Proxmox type yet. You can still add a direct
|
||||
connection manually.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<For each={activeDiscoveredNodes()}>
|
||||
{(server) => (
|
||||
<button
|
||||
type="button"
|
||||
class="w-full bg-surface-hover rounded-md p-4 border border-border text-left opacity-75 hover:opacity-100 hover:border-blue-300 dark:hover:border-blue-700 transition-all"
|
||||
onClick={() => openDiscoveredNode(server)}
|
||||
>
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-3 h-3 mt-1.5 rounded-full bg-gray-400 animate-pulse"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="font-medium text-base-content">
|
||||
{activeConfig().titleFromServer(server)}
|
||||
</h4>
|
||||
<p class="text-sm mt-1">
|
||||
{server.ip}:{server.port}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-400 rounded">
|
||||
Discovered
|
||||
</span>
|
||||
<span class="text-xs text-muted">
|
||||
Click to configure this connection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
class="mt-1"
|
||||
>
|
||||
<path
|
||||
d="M12 5v14m-7-7h14"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
<ProxmoxDiscoveryResultsCard
|
||||
activeConfig={activeConfig()}
|
||||
activeDiscoveredNodes={activeDiscoveredNodes()}
|
||||
discoveryScanStatus={props.discoveryScanStatus()}
|
||||
hasDiscoveryTimeouts={hasDiscoveryTimeouts()}
|
||||
onOpenDiscoveredNode={openDiscoveredNode}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.showDeleteNodeModal()}>
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
onClose={props.cancelDeleteNode}
|
||||
panelClass="max-w-lg"
|
||||
closeOnBackdrop={false}
|
||||
ariaLabel={`Remove ${props.nodePendingDeleteLabel()}`}
|
||||
>
|
||||
<Card padding="lg" class="w-full max-w-lg space-y-5">
|
||||
<SectionHeader
|
||||
title={`Remove ${props.nodePendingDeleteLabel()}`}
|
||||
size="md"
|
||||
class="mb-1"
|
||||
/>
|
||||
<div class="space-y-3 text-sm text-gray-600">
|
||||
<p>
|
||||
Removing this {props.nodePendingDeleteTypeLabel().toLowerCase()} also scrubs the
|
||||
Pulse footprint on the host — the proxy service, SSH key, API token, and bind mount
|
||||
are all cleaned up automatically.
|
||||
</p>
|
||||
<div class="rounded-md border border-blue-200 bg-blue-50 p-3 text-sm leading-relaxed dark:border-blue-800 dark:bg-blue-900 dark:text-blue-100">
|
||||
<p class="font-medium text-blue-900 dark:text-blue-100">What happens next</p>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-4 text-blue-800 dark:text-blue-200 text-sm">
|
||||
<li>Pulse removes the node entry and clears related alerts.</li>
|
||||
<li>
|
||||
{props.nodePendingDeleteHost() ? (
|
||||
<>
|
||||
The host <span class="font-semibold">{props.nodePendingDeleteHost()}</span>{' '}
|
||||
loses the proxy service, SSH key, and API token.
|
||||
</>
|
||||
) : (
|
||||
'The host loses the proxy service, SSH key, and API token.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
If the host comes back later, rerunning the setup script reinstalls everything
|
||||
with a fresh key.
|
||||
</li>
|
||||
<Show when={props.nodePendingDeleteType() === 'pbs'}>
|
||||
<li>
|
||||
Backup user tokens on the PBS are removed, so jobs referencing them will no
|
||||
longer authenticate until the node is re-added.
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={props.nodePendingDeleteType() === 'pmg'}>
|
||||
<li>
|
||||
Mail gateway tokens are removed as part of the cleanup; re-enroll to restore
|
||||
outbound telemetry.
|
||||
</li>
|
||||
</Show>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.cancelDeleteNode}
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
disabled={props.deleteNodeLoading()}
|
||||
>
|
||||
Keep node
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.deleteNode}
|
||||
disabled={props.deleteNodeLoading()}
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-red-500 dark:hover:bg-red-400"
|
||||
>
|
||||
{props.deleteNodeLoading() ? 'Removing…' : 'Remove node'}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</Dialog>
|
||||
<ProxmoxDeleteNodeDialog
|
||||
deleteNodeLoading={props.deleteNodeLoading()}
|
||||
nodePendingDeleteHost={props.nodePendingDeleteHost()}
|
||||
nodePendingDeleteLabel={props.nodePendingDeleteLabel()}
|
||||
nodePendingDeleteType={props.nodePendingDeleteType()}
|
||||
nodePendingDeleteTypeLabel={props.nodePendingDeleteTypeLabel()}
|
||||
onCancel={props.cancelDeleteNode}
|
||||
onDelete={props.deleteNode}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{renderNodeModal('pve')}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@ import unifiedAgentStatusPresentationSource from '@/utils/unifiedAgentStatusPres
|
|||
import unifiedAgentInventoryPresentationSource from '@/utils/unifiedAgentInventoryPresentation.ts?raw';
|
||||
import relayPresentationSource from '@/utils/relayPresentation.ts?raw';
|
||||
import relaySettingsPanelSource from '../RelaySettingsPanel.tsx?raw';
|
||||
import proxmoxDeleteNodeDialogSource from '../ProxmoxDeleteNodeDialog.tsx?raw';
|
||||
import proxmoxDirectConnectionsCardSource from '../ProxmoxDirectConnectionsCard.tsx?raw';
|
||||
import proxmoxDiscoveryResultsCardSource from '../ProxmoxDiscoveryResultsCard.tsx?raw';
|
||||
import proxmoxSettingsPanelSource from '../ProxmoxSettingsPanel.tsx?raw';
|
||||
import relayOnboardingCardSource from '@/components/Dashboard/RelayOnboardingCard.tsx?raw';
|
||||
import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw';
|
||||
|
|
@ -298,9 +301,16 @@ describe('monitored-system model guardrails', () => {
|
|||
expect(relaySettingsPanelSource).not.toContain('const connectionStatusVariant =');
|
||||
expect(relaySettingsPanelSource).not.toContain('const connectionStatusText =');
|
||||
expect(relaySettingsPanelSource).not.toContain('Loading configuration...');
|
||||
expect(proxmoxSettingsPanelSource).toContain('getSettingsConfigurationLoadingState');
|
||||
expect(proxmoxSettingsPanelSource).toContain('CalloutCard');
|
||||
expect(proxmoxSettingsPanelSource).not.toContain('Loading configuration...');
|
||||
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDirectConnectionsCard');
|
||||
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDiscoveryResultsCard');
|
||||
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDeleteNodeDialog');
|
||||
expect(proxmoxSettingsPanelSource).not.toContain('No discovery matches for this Proxmox type yet.');
|
||||
expect(proxmoxSettingsPanelSource).not.toContain('What happens next');
|
||||
expect(proxmoxDirectConnectionsCardSource).toContain('getSettingsConfigurationLoadingState');
|
||||
expect(proxmoxDiscoveryResultsCardSource).toContain('formatRelativeTime');
|
||||
expect(proxmoxDeleteNodeDialogSource).toContain('SectionHeader');
|
||||
expect(relayOnboardingCardSource).toContain('@/utils/relayPresentation');
|
||||
expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_TITLE');
|
||||
expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_DESCRIPTION');
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ import organizationOverviewPanelSource from '../OrganizationOverviewPanel.tsx?ra
|
|||
import organizationAccessPanelSource from '../OrganizationAccessPanel.tsx?raw';
|
||||
import organizationSharingPanelSource from '../OrganizationSharingPanel.tsx?raw';
|
||||
import organizationBillingPanelSource from '../OrganizationBillingPanel.tsx?raw';
|
||||
import proxmoxDeleteNodeDialogSource from '../ProxmoxDeleteNodeDialog.tsx?raw';
|
||||
import proxmoxDirectConnectionsCardSource from '../ProxmoxDirectConnectionsCard.tsx?raw';
|
||||
import proxmoxDiscoveryResultsCardSource from '../ProxmoxDiscoveryResultsCard.tsx?raw';
|
||||
import proxmoxSettingsPanelSource from '../ProxmoxSettingsPanel.tsx?raw';
|
||||
import securityOverviewPanelSource from '../SecurityOverviewPanel.tsx?raw';
|
||||
import securityAuthPanelSource from '../SecurityAuthPanel.tsx?raw';
|
||||
import ssoProvidersPanelSource from '../SSOProvidersPanel.tsx?raw';
|
||||
|
|
@ -93,6 +97,9 @@ const extractedModules = [
|
|||
'../useSSOProvidersState.ts',
|
||||
'../ssoProvidersModel.ts',
|
||||
'../ProxmoxSettingsPanel.tsx',
|
||||
'../ProxmoxDeleteNodeDialog.tsx',
|
||||
'../ProxmoxDirectConnectionsCard.tsx',
|
||||
'../ProxmoxDiscoveryResultsCard.tsx',
|
||||
'../SettingsDialogs.tsx',
|
||||
'../SettingsPageShell.tsx',
|
||||
'../useDiscoverySettingsState.ts',
|
||||
|
|
@ -305,6 +312,20 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(settingsLineCount).toBeLessThanOrEqual(maxSettingsLines);
|
||||
});
|
||||
|
||||
it('keeps the direct Proxmox settings workspace split into section owners', () => {
|
||||
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDirectConnectionsCard');
|
||||
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDiscoveryResultsCard');
|
||||
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDeleteNodeDialog');
|
||||
expect(proxmoxSettingsPanelSource).not.toContain('No discovery matches for this Proxmox type yet.');
|
||||
expect(proxmoxSettingsPanelSource).not.toContain('What happens next');
|
||||
expect(proxmoxDirectConnectionsCardSource).toContain('getSettingsConfigurationLoadingState');
|
||||
expect(proxmoxDiscoveryResultsCardSource).toContain('Discovery issues:');
|
||||
expect(proxmoxDiscoveryResultsCardSource).toContain(
|
||||
'No discovery matches for this Proxmox type yet. You can still add a direct',
|
||||
);
|
||||
expect(proxmoxDeleteNodeDialogSource).toContain('What happens next');
|
||||
});
|
||||
|
||||
it('uses lazy() imports for panel components in settingsPanelRegistry', async () => {
|
||||
const source = (await import('../settingsPanelRegistry.ts?raw')).default;
|
||||
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ import configuredNodeStatusPresentationSource from '@/utils/configuredNodeStatus
|
|||
import rbacPresentationSource from '@/utils/rbacPresentation.ts?raw';
|
||||
import upgradePresentationSource from '@/utils/upgradePresentation.ts?raw';
|
||||
import agentProfilesPanelSource from '@/components/Settings/AgentProfilesPanel.tsx?raw';
|
||||
import agentProfilesPanelStateSource from '@/components/Settings/useAgentProfilesPanelState.ts?raw';
|
||||
import agentProfilesPresentationSource from '@/utils/agentProfilesPresentation.ts?raw';
|
||||
import agentProfileSuggestionPresentationSource from '@/utils/agentProfileSuggestionPresentation.ts?raw';
|
||||
import organizationAccessPanelSource from '@/components/Settings/OrganizationAccessPanel.tsx?raw';
|
||||
|
|
@ -799,7 +800,8 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(reportingPanelSource).not.toContain('window.URL.createObjectURL');
|
||||
expect(rolesPanelSource).toContain('@/utils/upgradePresentation');
|
||||
expect(userAssignmentsPanelSource).toContain('@/utils/upgradePresentation');
|
||||
expect(agentProfilesPanelSource).toContain('@/utils/upgradePresentation');
|
||||
expect(agentProfilesPanelSource).toContain('./useAgentProfilesPanelState');
|
||||
expect(agentProfilesPanelStateSource).toContain('@/utils/upgradePresentation');
|
||||
expect(agentProfilesPanelSource).toContain('@/utils/agentProfilesPresentation');
|
||||
expect(agentProfilesPanelSource).toContain('getAgentProfilesEmptyState');
|
||||
expect(agentProfilesPanelSource).toContain('getAgentProfileAssignmentsEmptyState');
|
||||
|
|
|
|||
|
|
@ -1838,6 +1838,48 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
def _assert_direct_proxmox_workspace_change_requires_agent_lifecycle(
|
||||
self, touched_path: str
|
||||
) -> None:
|
||||
required = infer_impacted_subsystems([touched_path])
|
||||
self.assertEqual(set(required), {"agent-lifecycle"})
|
||||
|
||||
lifecycle = required["agent-lifecycle"]
|
||||
self.assertEqual(
|
||||
lifecycle["contract"],
|
||||
"docs/release-control/v6/internal/subsystems/agent-lifecycle.md",
|
||||
)
|
||||
self.assertEqual(lifecycle["touched_runtime_files"], [touched_path])
|
||||
self.assertEqual(
|
||||
lifecycle["verification_requirements"],
|
||||
[
|
||||
{
|
||||
"id": "direct-proxmox-workspace-surface",
|
||||
"label": "direct Proxmox workspace lifecycle proof",
|
||||
"touched_runtime_files": [touched_path],
|
||||
"allow_same_subsystem_tests": False,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx",
|
||||
"frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx",
|
||||
"frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts",
|
||||
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts",
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_proxmox_settings_panel_change_requires_agent_lifecycle(self):
|
||||
self._assert_direct_proxmox_workspace_change_requires_agent_lifecycle(
|
||||
"frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx"
|
||||
)
|
||||
|
||||
def test_infrastructure_settings_state_change_requires_agent_lifecycle(self):
|
||||
self._assert_direct_proxmox_workspace_change_requires_agent_lifecycle(
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts"
|
||||
)
|
||||
|
||||
def test_agent_install_backend_change_requires_lifecycle_and_api_contracts(self):
|
||||
required = infer_impacted_subsystems(["internal/api/agent_install_command_shared.go"])
|
||||
self.assertEqual(set(required), {"agent-lifecycle", "api-contracts"})
|
||||
|
|
|
|||
|
|
@ -1219,6 +1219,40 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_proxmox_settings_panel_to_agent_lifecycle(self) -> None:
|
||||
result = lookup_paths(["frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx"])
|
||||
self.assertEqual(result["unowned_runtime_files"], [])
|
||||
self.assertEqual(
|
||||
{item["subsystem"] for item in result["impacted_subsystems"]},
|
||||
{"agent-lifecycle"},
|
||||
)
|
||||
file_entry = result["files"][0]
|
||||
self.assertEqual(file_entry["classification"], "runtime")
|
||||
self.assertEqual(
|
||||
{match["subsystem"] for match in file_entry["matches"]},
|
||||
{"agent-lifecycle"},
|
||||
)
|
||||
match = file_entry["matches"][0]
|
||||
self.assertEqual(
|
||||
match["contract"],
|
||||
"docs/release-control/v6/internal/subsystems/agent-lifecycle.md",
|
||||
)
|
||||
self.assertEqual(match["lane_context"]["lane_id"], "L16")
|
||||
self.assertEqual(
|
||||
match["verification_requirement"]["id"],
|
||||
"direct-proxmox-workspace-surface",
|
||||
)
|
||||
self.assertEqual(
|
||||
match["verification_requirement"]["exact_files"],
|
||||
[
|
||||
"frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx",
|
||||
"frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx",
|
||||
"frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts",
|
||||
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts",
|
||||
],
|
||||
)
|
||||
|
||||
def test_lookup_paths_reports_agent_install_backend_as_shared_boundary(self) -> None:
|
||||
result = lookup_paths(["internal/api/agent_install_command_shared.go"])
|
||||
self.assertEqual(result["unowned_runtime_files"], [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue