v6(settings): phase 6a — PVE/PBS/PMG credential slot inside ConnectionEditor

Replaces the ProxmoxSettingsPanel stopgap that rendered an entire
Proxmox workspace (discovery card, configured nodes table, node modal
stack) inside the add-connection credential slot. The add flow now
mounts NodeCredentialSlot, which drops the Dialog chrome and reuses the
NodeModalBasicInfoSection / AuthenticationSection / MonitoringSection /
StatusFooter primitives directly so the user sees a fresh credentials
form — not the configured-nodes table they just came from.

Save dispatch still routes through the existing per-type saveNode
plumbing; the unified form is additive and does not change backend
contracts.
This commit is contained in:
rcourtman 2026-04-19 13:38:53 +01:00
parent a1f8dbe8f9
commit 4a587ba544
5 changed files with 109 additions and 23 deletions

View file

@ -321,11 +321,21 @@ an add-only capacity posture.
infrastructure workspace in place, headed by a back-to-systems
affordance. The editor's probe step calls the aggregator probe
endpoint and dispatches the detected or manually-selected type into a
credential slot that renders the existing per-type panel; it must not
bypass the probe endpoint or fabricate probe candidates, and the
agent credential slot must continue to reach
credential slot; it must not bypass the probe endpoint or fabricate
probe candidates, and the agent credential slot must continue to reach
`InfrastructureInstallerSection.tsx` so install handoffs remain on the
canonical unified-agent install path.
canonical unified-agent install path. For PVE, PBS, and PMG, the
credential slot is
`frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`,
which reuses the existing `NodeModalBasicInfoSection`,
`NodeModalAuthenticationSection`, `NodeModalMonitoringSection`, and
`NodeModalStatusFooter` primitives inline under the editor — dropping
the Dialog wrapper and the surrounding discovery/configured-nodes
workspace. The add flow must not reintroduce the full Proxmox
workspace (discovery card, configured nodes table, node-modal stack)
into the credential slot, because that previously showed the
ledger-of-other-systems in the middle of entering one system's
credentials.
## Forbidden Paths

View file

@ -234,13 +234,21 @@ work extends shared components instead of creating new local variants.
`frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`,
not through a per-type picker or per-type step tree. The editor's probe
step is the canonical entry point; credential slots are dispatched by the
detected or manually-selected type and must reach the existing per-type
settings panel rather than diverging into a parallel add surface. The
add flow must render inline in place of the inventory table, not as a
right-side drawer, centered modal, or other overlay chrome — placing
isolated per-type forms behind a common drawer is re-wrapping, and the
unified add surface is specifically not allowed to reintroduce that
pattern.
detected or manually-selected type and must reach an inline credential
form rather than diverging into a parallel add surface. The add flow
must render inline in place of the inventory table, not as a right-side
drawer, centered modal, or other overlay chrome — placing isolated
per-type forms behind a common drawer is re-wrapping, and the unified
add surface is specifically not allowed to reintroduce that pattern.
For PVE, PBS, and PMG, the credential slot is
`frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`
and it must compose `NodeModalBasicInfoSection`,
`NodeModalAuthenticationSection`, `NodeModalMonitoringSection`, and
`NodeModalStatusFooter` inline under the editor shell rather than
embedding the full Proxmox workspace (discovery card, configured
nodes table, delete dialog, node modal stack). Showing a ledger of
other systems inside the credential slot is exactly the ledger-inside-
editor drift this contract forbids.
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.

View file

@ -0,0 +1,66 @@
import { Component } from 'solid-js';
import type { NodeConfig, NodeConfigWithStatus } from '@/types/nodes';
import { NodeModalAuthenticationSection } from '@/components/Settings/NodeModalAuthenticationSection';
import { NodeModalBasicInfoSection } from '@/components/Settings/NodeModalBasicInfoSection';
import { NodeModalMonitoringSection } from '@/components/Settings/NodeModalMonitoringSection';
import { NodeModalStatusFooter } from '@/components/Settings/NodeModalStatusFooter';
import { useNodeModalState } from '@/components/Settings/useNodeModalState';
import type { NodeModalProps } from '@/components/Settings/nodeModalModel';
import type { InfrastructurePlatformSettingsProps } from '@/components/Settings/proxmoxSettingsModel';
export type NodeSlotType = 'pve' | 'pbs' | 'pmg';
export interface NodeCredentialSlotProps {
nodeType: NodeSlotType;
settings: InfrastructurePlatformSettingsProps;
editingNode?: NodeConfigWithStatus | null;
prefillNode?: Partial<NodeConfig>;
initialAddress?: string;
onCancel: () => void;
onSaved: () => void;
}
export const NodeCredentialSlot: Component<NodeCredentialSlotProps> = (props) => {
const prefill: Partial<NodeConfig> | undefined = props.prefillNode ?? (
props.initialAddress ? { host: props.initialAddress } : undefined
);
const handleSave = async (nodeData: Partial<NodeConfig>) => {
await props.settings.saveNode(nodeData);
props.onSaved();
};
const modalProps: NodeModalProps = {
isOpen: true,
nodeType: props.nodeType,
editingNode: props.editingNode ?? undefined,
prefillNode: prefill,
onClose: props.onCancel,
onSave: handleSave,
securityStatus: props.settings.securityStatus() ?? undefined,
temperatureMonitoringEnabled: props.settings.resolveTemperatureMonitoringEnabled(
props.editingNode ?? null,
),
temperatureMonitoringLocked: props.settings.temperatureMonitoringLocked(),
savingTemperatureSetting: props.settings.savingTemperatureSetting(),
onToggleTemperatureMonitoring: async (enabled) => {
const target = props.editingNode;
if (target?.id) {
await props.settings.handleNodeTemperatureMonitoringChange(target.id, enabled);
} else {
await props.settings.handleTemperatureMonitoringChange(enabled);
}
},
};
const state = useNodeModalState(modalProps);
return (
<form onSubmit={state.handleSubmit} class="space-y-6">
<NodeModalBasicInfoSection modalProps={modalProps} state={state} />
<NodeModalAuthenticationSection modalProps={modalProps} state={state} />
<NodeModalMonitoringSection modalProps={modalProps} state={state} />
<NodeModalStatusFooter modalProps={modalProps} state={state} />
</form>
);
};

View file

@ -10,12 +10,12 @@ import {
type SystemManageAction,
} from './connectionsTableModel';
import { ConnectionEditor } from './ConnectionEditor/ConnectionEditor';
import { NodeCredentialSlot } from './ConnectionEditor/CredentialSlots/NodeCredentialSlot';
import type { ConnectionType } from '@/api/connections';
import { InfrastructureActiveRowDetails } from './InfrastructureActiveRowDetails';
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
import { InfrastructureIgnoredRowDetails } from './InfrastructureIgnoredRowDetails';
import { InfrastructureStopMonitoringDialog } from './InfrastructureStopMonitoringDialog';
import { ProxmoxSettingsPanel } from './ProxmoxSettingsPanel';
import { TrueNASSettingsPanel } from './TrueNASSettingsPanel';
import { VMwareSettingsPanel } from './VMwareSettingsPanel';
import {
@ -24,7 +24,6 @@ import {
type InfrastructureAddStep,
} from './infrastructureWorkspaceModel';
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
import type { NodeType } from './infrastructureSettingsModel';
import {
InfrastructureOperationsStateProvider,
useInfrastructureOperationsContext,
@ -127,14 +126,13 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
setShowAgentProfiles(false);
};
const renderProxmoxSlot = (type: NodeType) => {
const selectedAgent = () => type;
const renderNodeSlot = (type: 'pve' | 'pbs' | 'pmg') => {
return (
<ProxmoxSettingsPanel
{...props}
selectedAgent={selectedAgent}
onSelectAgent={() => {}}
embedded
<NodeCredentialSlot
nodeType={type}
settings={props}
onCancel={exitAddMode}
onSaved={exitAddMode}
/>
);
};
@ -178,7 +176,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
case 'pve':
case 'pbs':
case 'pmg':
return renderProxmoxSlot(type);
return renderNodeSlot(type);
case 'truenas':
return <TrueNASSettingsPanel state={props.trueNASSettings} />;
case 'vmware':

View file

@ -54,8 +54,12 @@ vi.mock('../InfrastructureInstallerSection', () => ({
InfrastructureInstallerSection: () => <div data-testid="install-section">install</div>,
}));
vi.mock('../ProxmoxSettingsPanel', () => ({
ProxmoxSettingsPanel: () => <div data-testid="proxmox-section">proxmox</div>,
vi.mock('../ConnectionEditor/CredentialSlots/NodeCredentialSlot', () => ({
NodeCredentialSlot: (props: { nodeType: string }) => (
<div data-testid="proxmox-section" data-node-type={props.nodeType}>
proxmox
</div>
),
}));
vi.mock('../TrueNASSettingsPanel', () => ({