mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Align test suite with single-route infrastructure architecture
Updates all test files broken by the collapse of infrastructure sub-routes (/install, /platforms, /connections) into one /settings/infrastructure entry. Key contract deltas: - settingsNavCatalog/Model: infrastructure-connections and infrastructure-install tabs removed; infrastructure-systems is the single sidebar entry - infrastructureWorkspaceModel: getInfrastructureWorkspaceViewFromPath renamed to deriveAddStepFromLegacyPath; buildInfrastructureWorkspacePath now always returns /settings/infrastructure regardless of argument - settingsNavigationModel: deriveAgentFromPath and isProxmoxSettingsPath use normalised (not canonical) path so Proxmox deep links still resolve to the correct agent before the canonical-path redirect fires - useSettingsNavigation: Proxmox agent sync moved before the redirect early- return so deep links seed the correct selectedAgent - InfrastructureInstallerSection: "Open Platform connections" button replaced with informational text; navigate target updated to flat workspace path - useInfrastructureDiscoveryRuntimeState: currentTab parameter removed (tab collapse means discovery is always active when the workspace is mounted) - SetupCompletionPanel: unified INFRASTRUCTURE_PATH constant replaces the two distinct INFRASTRUCTURE_INSTALL_PATH / INFRASTRUCTURE_PLATFORMS_PATH - All navigation call sites (Dashboard, DashboardStateCards, Infrastructure empty-state) updated to /settings/infrastructure - Contract updates: agent-lifecycle, api-contracts, frontend-primitives
This commit is contained in:
parent
9bac3f421d
commit
fa2768ae78
32 changed files with 532 additions and 578 deletions
|
|
@ -451,6 +451,12 @@ an add-only capacity posture.
|
|||
|
||||
## Current State
|
||||
|
||||
The infrastructure workspace collapsed to a single `/settings/infrastructure`
|
||||
route. `buildInfrastructureWorkspacePath()` always returns the base path;
|
||||
`deriveAddStepFromLegacyPath()` maps legacy sub-paths to in-page panel state.
|
||||
`SetupCompletionPanel.tsx` uses one `INFRASTRUCTURE_PATH` constant for all
|
||||
install and platform CTAs.
|
||||
|
||||
This subsystem now sits under the dedicated agent lifecycle and fleet
|
||||
operations lane so install, registration, update continuity, profile
|
||||
management, and fleet safety stop hiding inside architecture, migration, or
|
||||
|
|
@ -2185,3 +2191,21 @@ test hooks, command-client factories, and timing overrides on the per-config or
|
|||
per-agent instance instead of package-global variables, so concurrent
|
||||
lifecycle-owned update and registration paths cannot leak one test or runtime
|
||||
override into another agent session.
|
||||
The infrastructure workspace now uses a single flat route
|
||||
(`/settings/infrastructure`) instead of the former three-sub-route layout
|
||||
(`/install`, `/platforms`, `/connections`). Panel state — which of the picker,
|
||||
agent-install, or platform-specific screens is open — is managed in-page
|
||||
through `InfrastructurePanelStep` signals rather than URL segments.
|
||||
`frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
|
||||
exposes `buildInfrastructureWorkspacePath()` (always returns the base path
|
||||
regardless of argument) and `deriveAddStepFromLegacyPath()` (maps legacy deep
|
||||
URLs to panel state for backwards-compatible deep-link resolution) as the sole
|
||||
path-building and path-reading contract for lifecycle-adjacent install and
|
||||
setup surfaces. Callers that formerly passed `'platforms'` or `'install'` to
|
||||
`buildInfrastructureWorkspacePath` must use the no-argument form; the argument
|
||||
is accepted but ignored so link-site callers can be updated incrementally
|
||||
without breaking navigation.
|
||||
`frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx` now uses
|
||||
a single `INFRASTRUCTURE_PATH` constant (`/settings/infrastructure`) for all
|
||||
install and platform-connection CTAs, replacing the former pair of
|
||||
`INFRASTRUCTURE_INSTALL_PATH` and `INFRASTRUCTURE_PLATFORMS_PATH` constants.
|
||||
|
|
|
|||
|
|
@ -526,6 +526,11 @@ the canonical monitored-system blocked payload.
|
|||
|
||||
## Current State
|
||||
|
||||
`useInfrastructureDiscoveryRuntimeState.ts` no longer gates `/api/discover`
|
||||
polling on a settings tab name; polling is mount-scoped. The tab guard was
|
||||
removed when the infrastructure nav collapsed to one `infrastructure-systems`
|
||||
entry.
|
||||
|
||||
The API layer already uses contract tests in many places, but every major live
|
||||
contract should continue moving toward canonical-only runtime shapes.
|
||||
That same shared settings/licensing contract now also owns the split usage-data
|
||||
|
|
@ -2893,3 +2898,12 @@ payloads without changing the runtime truth. Patrol transport may omit
|
|||
only after canonical IDs have been resolved or a safe row-name fallback has
|
||||
been chosen. API-adjacent browser callers must not reinterpret missing IDs or
|
||||
preview arrays as authoritative empty success.
|
||||
That same shared browser transport contract now also owns the discovery polling
|
||||
mount scope.
|
||||
`frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`
|
||||
no longer gates `/api/discover` polling on the settings tab name; polling
|
||||
starts whenever the hook is mounted and stops on cleanup. Callers must not
|
||||
re-introduce a per-tab gate on this boundary. The discovery subnet settings
|
||||
write path through `SettingsAPI.updateSystemSettings` remains governed by the
|
||||
shared `internal/api/` settings boundary and is unaffected by the polling
|
||||
scope change.
|
||||
|
|
|
|||
|
|
@ -572,6 +572,12 @@ connections` visible as the API-backed alternative for Proxmox and
|
|||
|
||||
## Current State
|
||||
|
||||
`SettingsTab` no longer includes `infrastructure-connections` or
|
||||
`infrastructure-install`. The single `infrastructure-systems` entry in
|
||||
`settingsNavCatalog.ts`, `settingsPanelRegistry.ts`, and
|
||||
`settingsNavigationModel.ts` replaces both. Panel routing within the
|
||||
infrastructure area uses `InfrastructurePanelStep` in-page state.
|
||||
|
||||
The frontend already has several guardrail tests. The next step is to keep
|
||||
turning repeated local patterns into explicit shared primitives with hard usage
|
||||
bounds, including provider-backed alert-history wording. `frontend-modern/src/features/alerts/helpers.ts`,
|
||||
|
|
@ -2238,3 +2244,18 @@ shared buttons must preserve discriminated disclosure props, toggle and a11y
|
|||
helpers must expose exact event signatures, shared rows must accept typed
|
||||
`data-*` props, and reporting-panel helpers must remain ES2020-safe instead of
|
||||
depending on feature-local casts or newer string helpers.
|
||||
The settings navigation model now exposes a single `infrastructure-systems`
|
||||
sidebar entry for the infrastructure settings area. The former
|
||||
`infrastructure-connections` and `infrastructure-install` entries have been
|
||||
removed from `SettingsTab`, `settingsNavCatalog.ts`, `settingsPanelRegistry.ts`,
|
||||
and `settingsNavigationModel.ts`. All canonical redirects and tab-derivation
|
||||
logic that previously mapped to those two entries now collapse to
|
||||
`infrastructure-systems`. No future additions to the settings nav may restore
|
||||
`infrastructure-connections` or `infrastructure-install` as independent tab
|
||||
identifiers; panel routing within the infrastructure area must use
|
||||
`InfrastructurePanelStep` in-page state instead of URL sub-routes.
|
||||
`frontend-modern/src/components/Settings/settingsNavigationModel.ts` now uses
|
||||
the normalised (not canonical) path when resolving Proxmox agent and path
|
||||
checks so that deep links such as `/settings/infrastructure/platforms/proxmox/pbs`
|
||||
resolve to the correct agent before the canonical-redirect fires, rather than
|
||||
after it has already collapsed the path.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,6 @@ describe('DashboardStateCards', () => {
|
|||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open infrastructure setup' }));
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith('/settings/infrastructure/install');
|
||||
expect(navigate).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -465,15 +465,8 @@ export const InfrastructureInstallerSection: Component = () => {
|
|||
{state.getSelectedInstallProfile().description}
|
||||
</p>
|
||||
<p class="mt-1.5 text-xs text-muted">
|
||||
API-backed platforms such as TrueNAS connect through Platform connections
|
||||
rather than a dedicated install profile here.{' '}
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.openPlatformConnections}
|
||||
class="text-blue-600 underline hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Open Platform connections
|
||||
</button>
|
||||
API-backed platforms such as TrueNAS connect through Add infrastructure →
|
||||
choose the platform type.
|
||||
</p>
|
||||
<Show when={state.getInstallProfileFlags().length > 0}>
|
||||
<p class="mt-1.5 text-xs text-muted">
|
||||
|
|
|
|||
|
|
@ -14,12 +14,17 @@ import { InfrastructureActiveRowDetails } from './InfrastructureActiveRowDetails
|
|||
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
|
||||
import { InfrastructureIgnoredRowDetails } from './InfrastructureIgnoredRowDetails';
|
||||
import { InfrastructureStopMonitoringDialog } from './InfrastructureStopMonitoringDialog';
|
||||
import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace';
|
||||
import { ProxmoxSettingsPanel } from './ProxmoxSettingsPanel';
|
||||
import { TrueNASSettingsPanel } from './TrueNASSettingsPanel';
|
||||
import { VMwareSettingsPanel } from './VMwareSettingsPanel';
|
||||
import {
|
||||
buildInfrastructureWorkspacePath,
|
||||
getInfrastructureWorkspaceViewFromPath,
|
||||
deriveAddStepFromLegacyPath,
|
||||
type InfrastructureAddStep,
|
||||
type InfrastructurePanelStep,
|
||||
} from './infrastructureWorkspaceModel';
|
||||
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
|
||||
import type { NodeType } from './infrastructureSettingsModel';
|
||||
import {
|
||||
InfrastructureOperationsStateProvider,
|
||||
useInfrastructureOperationsContext,
|
||||
|
|
@ -27,17 +32,49 @@ import {
|
|||
|
||||
export type InfrastructureWorkspaceProps = InfrastructurePlatformSettingsProps;
|
||||
|
||||
const proxmoxRouteForKind = (kind: 'pve' | 'pbs' | 'pmg') =>
|
||||
`/settings/infrastructure/platforms/proxmox/${kind}`;
|
||||
const STEP_TITLES: Record<InfrastructureAddStep, string> = {
|
||||
agent: 'Install on a host',
|
||||
pve: 'Proxmox VE',
|
||||
pbs: 'Proxmox Backup Server',
|
||||
pmg: 'Proxmox Mail Gateway',
|
||||
truenas: 'TrueNAS SCALE',
|
||||
vmware: 'VMware vSphere / ESXi',
|
||||
};
|
||||
|
||||
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 [showInstallProfiles, setShowInstallProfiles] = createSignal(false);
|
||||
const [panelStep, setPanelStep] = createSignal<InfrastructurePanelStep | null>(null);
|
||||
const [showAgentProfiles, setShowAgentProfiles] = createSignal(false);
|
||||
const readOnly = createMemo(() => presentationPolicyIsReadOnly());
|
||||
|
||||
// Redirect legacy deep links and open the appropriate panel.
|
||||
createEffect(() => {
|
||||
const path = location.pathname;
|
||||
if (path === '/settings/infrastructure') return;
|
||||
|
||||
const step = deriveAddStepFromLegacyPath(path);
|
||||
navigate(buildInfrastructureWorkspacePath(), { replace: true });
|
||||
if (step && !readOnly()) {
|
||||
setPanelStep(step);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-open the agent installer when a setup handoff is waiting.
|
||||
createEffect(() => {
|
||||
if (state.setupHandoff?.() && panelStep() === null && !readOnly()) {
|
||||
setPanelStep('agent');
|
||||
}
|
||||
});
|
||||
|
||||
// Close add panel in read-only mode.
|
||||
createEffect(() => {
|
||||
if (readOnly() && panelStep() !== null) {
|
||||
setPanelStep(null);
|
||||
}
|
||||
});
|
||||
|
||||
const rows = createMemo<InfrastructureSystemRow[]>(() =>
|
||||
buildInfrastructureSystemRows({
|
||||
|
|
@ -45,52 +82,21 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
monitoringStoppedRows: state.monitoringStoppedRows(),
|
||||
}),
|
||||
);
|
||||
|
||||
const headerActions = createMemo<ConnectionsTableHeaderAction[]>(() =>
|
||||
readOnlyWorkspace()
|
||||
readOnly()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: 'Add infrastructure',
|
||||
onSelect: () => navigate(buildInfrastructureWorkspacePath('install'), { scroll: false }),
|
||||
onSelect: () => {
|
||||
setPanelStep('pick');
|
||||
setShowAgentProfiles(false);
|
||||
},
|
||||
tone: 'primary' as const,
|
||||
},
|
||||
],
|
||||
);
|
||||
const closeConnectionsWorkspace = () => {
|
||||
props.trueNASSettings.closeDialog?.();
|
||||
props.trueNASSettings.closeDeleteDialog?.();
|
||||
props.vmwareSettings.closeDialog?.();
|
||||
props.vmwareSettings.closeDeleteDialog?.();
|
||||
props.setShowNodeModal(false);
|
||||
props.setEditingNode(null);
|
||||
props.cancelDeleteNode?.();
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'), { scroll: false });
|
||||
};
|
||||
const closeInstallWorkspace = () => {
|
||||
setShowInstallProfiles(false);
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'), { scroll: false });
|
||||
};
|
||||
|
||||
const handleAddSystem = (choice: AddSystemChoice) => {
|
||||
setShowInstallProfiles(false);
|
||||
|
||||
if (choice.kind === 'agent') {
|
||||
navigate(buildInfrastructureWorkspacePath('install'), { scroll: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.kind === 'truenas') {
|
||||
navigate('/settings/infrastructure/platforms/truenas', { scroll: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.kind === 'vmware') {
|
||||
navigate('/settings/infrastructure/platforms/vmware', { scroll: false });
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(proxmoxRouteForKind(choice.kind), { scroll: false });
|
||||
};
|
||||
|
||||
const handleManageAction = (action: SystemManageAction) => {
|
||||
switch (action.kind) {
|
||||
|
|
@ -107,137 +113,191 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (readOnlyWorkspace() && activeView() !== 'inventory') {
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'), { replace: true });
|
||||
}
|
||||
const handleAddSystem = (choice: AddSystemChoice) => {
|
||||
setShowAgentProfiles(false);
|
||||
setPanelStep(choice.kind as InfrastructureAddStep);
|
||||
};
|
||||
|
||||
const closePanel = () => {
|
||||
setPanelStep(null);
|
||||
setShowAgentProfiles(false);
|
||||
};
|
||||
|
||||
const goBackToPick = () => {
|
||||
setShowAgentProfiles(false);
|
||||
setPanelStep('pick');
|
||||
};
|
||||
|
||||
// For Proxmox: selectedAgent is derived from the active panel step.
|
||||
const proxmoxAgent = createMemo<NodeType>(() => {
|
||||
const step = panelStep();
|
||||
if (step === 'pbs') return 'pbs';
|
||||
if (step === 'pmg') return 'pmg';
|
||||
return 'pve';
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (activeView() === 'inventory') {
|
||||
return;
|
||||
}
|
||||
if (activeView() !== 'install') {
|
||||
setShowInstallProfiles(false);
|
||||
}
|
||||
state.setExpandedRowKey(null);
|
||||
state.setSelectedIgnoredRowKey(null);
|
||||
});
|
||||
const onSelectProxmoxAgent = (agent: NodeType) => {
|
||||
setPanelStep(agent as InfrastructureAddStep);
|
||||
};
|
||||
|
||||
const isProxmoxStep = () => {
|
||||
const step = panelStep();
|
||||
return step === 'pve' || step === 'pbs' || step === 'pmg';
|
||||
};
|
||||
|
||||
const stepTitle = () => {
|
||||
const step = panelStep();
|
||||
if (!step || step === 'pick') return 'Add infrastructure';
|
||||
return STEP_TITLES[step];
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-8">
|
||||
<Show when={activeView() === 'inventory'}>
|
||||
<ConnectionsTable
|
||||
rows={rows}
|
||||
headerActions={headerActions()}
|
||||
onManageRow={(row) => handleManageAction(row.manage)}
|
||||
/>
|
||||
</Show>
|
||||
<ConnectionsTable
|
||||
rows={rows}
|
||||
headerActions={headerActions()}
|
||||
onManageRow={(row) => handleManageAction(row.manage)}
|
||||
/>
|
||||
|
||||
<Show when={!readOnlyWorkspace() && activeView() === 'install'}>
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeInstallWorkspace}
|
||||
class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Back to monitored systems
|
||||
</button>
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
|
||||
Add infrastructure
|
||||
</div>
|
||||
<div class="text-xl font-semibold text-base-content">Choose what to connect</div>
|
||||
<div class="text-sm text-muted">
|
||||
Start with a host install or jump straight to an API-backed platform connection.
|
||||
</div>
|
||||
</div>
|
||||
{/* Add-infrastructure panel — right drawer */}
|
||||
<Dialog
|
||||
isOpen={panelStep() !== null}
|
||||
onClose={closePanel}
|
||||
layout="drawer-right"
|
||||
panelClass="max-w-[820px]"
|
||||
ariaLabel={stepTitle()}
|
||||
>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
{/* Drawer header */}
|
||||
<div class="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={panelStep() !== 'pick'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBackToPick}
|
||||
class="inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
← Choose different type
|
||||
</button>
|
||||
</Show>
|
||||
<span class="text-sm font-semibold text-base-content">{stepTitle()}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={panelStep() === 'agent'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAgentProfiles((v) => !v)}
|
||||
class="inline-flex items-center rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
{showAgentProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInstallProfiles((value) => !value)}
|
||||
class="inline-flex items-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
onClick={closePanel}
|
||||
class="rounded-md p-1 text-muted hover:bg-surface-hover hover:text-base-content"
|
||||
aria-label="Close"
|
||||
>
|
||||
{showInstallProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
<For each={ADD_SYSTEM_CHOICES}>
|
||||
{(choice) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddSystem(choice)}
|
||||
class={`flex h-full flex-col items-start justify-between gap-4 rounded-xl border px-4 py-4 text-left transition-colors ${
|
||||
choice.kind === 'agent'
|
||||
? 'border-blue-300 bg-blue-50/60 text-base-content dark:border-blue-700 dark:bg-blue-950/20'
|
||||
: 'border-border bg-surface hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<div class="text-sm font-semibold text-base-content">{choice.title}</div>
|
||||
<div class="text-xs text-muted">{choice.description}</div>
|
||||
{/* Drawer content */}
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{/* Step 1: picker */}
|
||||
<Show when={panelStep() === 'pick'}>
|
||||
<div class="space-y-4 p-4">
|
||||
<p class="text-sm text-muted">
|
||||
Choose the system or platform you want Pulse to start monitoring.
|
||||
</p>
|
||||
<ul class="divide-y divide-border rounded-md border border-border">
|
||||
<For each={ADD_SYSTEM_CHOICES}>
|
||||
{(choice) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddSystem(choice)}
|
||||
class="flex w-full items-start justify-between gap-4 px-4 py-3 text-left hover:bg-surface-hover"
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{choice.title}
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-muted">{choice.description}</div>
|
||||
</div>
|
||||
<span
|
||||
class={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
choice.method === 'api'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
|
||||
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
|
||||
}`}
|
||||
>
|
||||
{choice.methodLabel}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2a: agent installer */}
|
||||
<Show when={panelStep() === 'agent'}>
|
||||
<div class="space-y-4 p-4">
|
||||
<Show when={showAgentProfiles()}>
|
||||
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
||||
<div class="mb-4 space-y-1">
|
||||
<div class="text-base font-semibold text-base-content">Agent profiles</div>
|
||||
<div class="text-sm text-muted">
|
||||
Manage reusable install defaults for agent-based systems.
|
||||
</div>
|
||||
</div>
|
||||
<AgentProfilesPanel />
|
||||
</div>
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
choice.method === 'api'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
|
||||
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
|
||||
}`}
|
||||
>
|
||||
{choice.methodLabel}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm sm:p-6">
|
||||
<InfrastructureInstallerSection />
|
||||
</div>
|
||||
|
||||
<Show when={showInstallProfiles()}>
|
||||
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm sm:p-6">
|
||||
<div class="mb-4 space-y-1">
|
||||
<div class="text-lg font-semibold text-base-content">Agent profiles</div>
|
||||
<div class="text-sm text-muted">
|
||||
Manage reusable install defaults for agent-based systems.
|
||||
</div>
|
||||
</Show>
|
||||
<InfrastructureInstallerSection />
|
||||
</div>
|
||||
<AgentProfilesPanel />
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
{/* Step 2b: Proxmox (PVE / PBS / PMG) */}
|
||||
<Show when={isProxmoxStep()}>
|
||||
<div class="p-4">
|
||||
<ProxmoxSettingsPanel
|
||||
{...props}
|
||||
selectedAgent={proxmoxAgent}
|
||||
onSelectAgent={onSelectProxmoxAgent}
|
||||
embedded
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2c: TrueNAS */}
|
||||
<Show when={panelStep() === 'truenas'}>
|
||||
<div class="p-4">
|
||||
<TrueNASSettingsPanel state={props.trueNASSettings} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2d: VMware */}
|
||||
<Show when={panelStep() === 'vmware'}>
|
||||
<div class="p-4">
|
||||
<VMwareSettingsPanel state={props.vmwareSettings} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!readOnlyWorkspace() && activeView() === 'platforms'}>
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeConnectionsWorkspace}
|
||||
class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Back to monitored systems
|
||||
</button>
|
||||
<div class="space-y-1">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
|
||||
Connections
|
||||
</div>
|
||||
<div class="text-xl font-semibold text-base-content">Platform connections</div>
|
||||
<div class="text-sm text-muted">
|
||||
Configure API-backed providers in a full workspace instead of a modal overlay.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PlatformConnectionsWorkspace {...props} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<InfrastructureStopMonitoringDialog />
|
||||
</Dialog>
|
||||
|
||||
{/* Active system detail drawer */}
|
||||
<Dialog
|
||||
isOpen={Boolean(state.selectedActiveRow())}
|
||||
onClose={() => state.setExpandedRowKey(null)}
|
||||
|
|
@ -250,6 +310,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
</Show>
|
||||
</Dialog>
|
||||
|
||||
{/* Ignored system detail drawer */}
|
||||
<Dialog
|
||||
isOpen={Boolean(state.selectedIgnoredRow())}
|
||||
onClose={() => state.setSelectedIgnoredRowKey(null)}
|
||||
|
|
@ -261,6 +322,8 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
{(rowAccessor) => <InfrastructureIgnoredRowDetails rowAccessor={rowAccessor} />}
|
||||
</Show>
|
||||
</Dialog>
|
||||
|
||||
<InfrastructureStopMonitoringDialog />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
const { state, connected: _connected } = useWebSocket();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { currentTab, activeTab, selectedAgent, setActiveTab, handleSelectAgent } =
|
||||
const { activeTab, selectedAgent, setActiveTab, handleSelectAgent } =
|
||||
useSettingsNavigation({
|
||||
navigate,
|
||||
location,
|
||||
|
|
@ -74,7 +74,6 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
});
|
||||
const infrastructureSettings = useInfrastructureSettingsState({
|
||||
eventBus,
|
||||
currentTab,
|
||||
discoveryEnabled: discoverySettings.discoveryEnabled,
|
||||
setDiscoveryEnabled: discoverySettings.setDiscoveryEnabled,
|
||||
discoverySubnet: discoverySettings.discoverySubnet,
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ describe('InfrastructureOperationsController ownership guardrails', () => {
|
|||
expect(infrastructureInstallStateSource).toContain('MonitoringAPI.getState()');
|
||||
expect(infrastructureInstallStateSource).toContain('./infrastructureWorkspaceModel');
|
||||
expect(infrastructureInstallStateSource).toContain(
|
||||
"navigate(buildInfrastructureWorkspacePath('platforms'))",
|
||||
"navigate(buildInfrastructureWorkspacePath())",
|
||||
);
|
||||
expect(infrastructureInstallStateSource).not.toContain(
|
||||
"navigate('/settings/infrastructure/proxmox')",
|
||||
|
|
@ -130,7 +130,7 @@ describe('InfrastructureOperationsController ownership guardrails', () => {
|
|||
);
|
||||
expect(infrastructureDiscoveryRuntimeStateSource).toContain("apiFetch('/api/discover'");
|
||||
expect(infrastructureDiscoveryRuntimeStateSource).toContain(
|
||||
"currentTab() === 'infrastructure-connections'",
|
||||
"discoveryEnabled: Accessor<boolean>",
|
||||
);
|
||||
expect(infrastructureDiscoveryRuntimeStateSource).toContain('SettingsAPI.updateSystemSettings');
|
||||
expect(infrastructureInstallerSectionSource).toContain('useInfrastructureOperationsContext');
|
||||
|
|
@ -2725,9 +2725,9 @@ describe('InfrastructureOperationsController platform commands', () => {
|
|||
|
||||
expect(screen.queryByRole('option', { name: 'TrueNAS SCALE agent' })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Open Platform connections/i }));
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/platforms');
|
||||
expect(
|
||||
screen.getByText(/API-backed platforms such as TrueNAS connect through Add infrastructure/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tracks install command copies', async () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { UnifiedAgentRow } from '../infrastructureOperationsModel';
|
|||
import { InfrastructureWorkspace } from '../InfrastructureWorkspace';
|
||||
|
||||
let mockPathname = '/settings/infrastructure';
|
||||
let mockSearch = '';
|
||||
const navigateSpy = vi.hoisted(() => vi.fn());
|
||||
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const setExpandedRowKeySpy = vi.hoisted(() => vi.fn());
|
||||
|
|
@ -19,7 +20,7 @@ vi.mock('@solidjs/router', async () => {
|
|||
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
|
||||
return {
|
||||
...actual,
|
||||
useLocation: () => ({ pathname: mockPathname }),
|
||||
useLocation: () => ({ pathname: mockPathname, search: mockSearch }),
|
||||
useNavigate: () => navigateSpy,
|
||||
};
|
||||
});
|
||||
|
|
@ -45,6 +46,7 @@ vi.mock('../useInfrastructureOperationsState', () => ({
|
|||
setSelectedIgnoredRowKeySpy(value);
|
||||
setSelectedIgnoredRowKey(value);
|
||||
},
|
||||
setupHandoff: () => null,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -52,8 +54,16 @@ vi.mock('../InfrastructureInstallerSection', () => ({
|
|||
InfrastructureInstallerSection: () => <div data-testid="install-section">install</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../PlatformConnectionsWorkspace', () => ({
|
||||
PlatformConnectionsWorkspace: () => <div data-testid="platform-section">platforms</div>,
|
||||
vi.mock('../ProxmoxSettingsPanel', () => ({
|
||||
ProxmoxSettingsPanel: () => <div data-testid="proxmox-section">proxmox</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../TrueNASSettingsPanel', () => ({
|
||||
TrueNASSettingsPanel: () => <div data-testid="truenas-section">truenas</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../VMwareSettingsPanel', () => ({
|
||||
VMwareSettingsPanel: () => <div data-testid="vmware-section">vmware</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../InfrastructureActiveRowDetails', () => ({
|
||||
|
|
@ -144,6 +154,7 @@ describe('InfrastructureWorkspace', () => {
|
|||
setCurrentNodeTypeSpy.mockReset();
|
||||
setModalResetKeySpy.mockReset();
|
||||
mockPathname = '/settings/infrastructure';
|
||||
mockSearch = '';
|
||||
mockActiveRows = [reportingRow()];
|
||||
mockIgnoredRows = [];
|
||||
setSelectedActiveRowKey(null);
|
||||
|
|
@ -157,19 +168,17 @@ describe('InfrastructureWorkspace', () => {
|
|||
const renderWorkspace = (propOverrides: Record<string, unknown> = {}) =>
|
||||
render(() => (<InfrastructureWorkspace {...{ ...baseProps(), ...propOverrides }} />) as any);
|
||||
|
||||
it('renders only the top-level ledger at the base infrastructure route', () => {
|
||||
it('renders the inventory table at the base infrastructure route', () => {
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Monitored systems' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Connections' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: 'Agent profiles' })).toBeNull();
|
||||
expect(screen.queryByTestId('platform-section')).toBeNull();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
expect(screen.queryByTestId('proxmox-section')).toBeNull();
|
||||
expect(screen.queryByTestId('agent-profiles')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows monitored systems only in the top ledger', () => {
|
||||
it('shows only monitored agents in the inventory table', () => {
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.getByText('tower')).toBeInTheDocument();
|
||||
|
|
@ -178,17 +187,90 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(screen.queryByText('lab-vcenter')).toBeNull();
|
||||
});
|
||||
|
||||
it('routes the add-infrastructure action to the install workspace', () => {
|
||||
it('opens the type picker in the add panel when Add infrastructure is clicked', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install', {
|
||||
scroll: false,
|
||||
});
|
||||
expect(screen.getByText('Choose the system or platform you want Pulse to start monitoring.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Install on a host/i })).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens reporting details from the top ledger in a drawer', () => {
|
||||
it('shows the agent installer when Install on a host is chosen', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
|
||||
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
expect(screen.queryByText('What do you want to connect?')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the TrueNAS panel when TrueNAS SCALE is chosen', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /TrueNAS SCALE/i }));
|
||||
|
||||
expect(screen.getByTestId('truenas-section')).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the VMware panel when VMware vSphere is chosen', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /VMware vSphere or ESXi/i }));
|
||||
|
||||
expect(screen.getByTestId('vmware-section')).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the Proxmox panel when Proxmox VE is chosen', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
|
||||
|
||||
expect(screen.getByTestId('proxmox-section')).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns to the picker via back button from a step-2 form', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Choose different type/i }));
|
||||
|
||||
expect(screen.getByText('Choose the system or platform you want Pulse to start monitoring.')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles agent profiles in the agent install drawer', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' }));
|
||||
|
||||
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not use modals or child-node setup, only sets panel state on no-modal choices', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
|
||||
|
||||
expect(setShowNodeModalSpy).not.toHaveBeenCalled();
|
||||
expect(setEditingNodeSpy).not.toHaveBeenCalled();
|
||||
expect(setCurrentNodeTypeSpy).not.toHaveBeenCalled();
|
||||
expect(setModalResetKeySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens reporting details from the inventory in a drawer', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View details' }));
|
||||
|
|
@ -197,82 +279,36 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(screen.getByTestId('active-details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders platform connections as a full-page workspace on platform deep links', () => {
|
||||
it('redirects legacy install deep link to the base route and opens the agent panel', () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects legacy platforms deep link to the base route and opens the picker', () => {
|
||||
mockPathname = '/settings/infrastructure/platforms/truenas';
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.queryByRole('heading', { name: 'Monitored systems' })).toBeNull();
|
||||
expect(screen.getByText('Platform connections')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('platform-section')).toBeInTheDocument();
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
|
||||
expect(screen.getByTestId('truenas-section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides Add infrastructure and the add panel in read-only mode', () => {
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Add infrastructure/i })).toBeNull();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders install tools as a full-page workspace on install deep links', () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.getByText('Choose what to connect')).toBeInTheDocument();
|
||||
expect(screen.getByText('Install on a host')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('platform-section')).toBeNull();
|
||||
});
|
||||
|
||||
it('opens agent profiles inline inside the install workspace', () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' }));
|
||||
|
||||
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns from platform connections to the monitored systems route', () => {
|
||||
mockPathname = '/settings/infrastructure/platforms/truenas';
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back to monitored systems' }));
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
|
||||
scroll: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('routes infrastructure choices through full-page setup routes instead of auto-opening dialogs', () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /TrueNAS SCALE/i }));
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas', {
|
||||
scroll: false,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /VMware vSphere or ESXi/i }));
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware', {
|
||||
scroll: false,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pve', {
|
||||
scroll: false,
|
||||
});
|
||||
|
||||
expect(setShowNodeModalSpy).not.toHaveBeenCalled();
|
||||
expect(setEditingNodeSpy).not.toHaveBeenCalled();
|
||||
expect(setCurrentNodeTypeSpy).not.toHaveBeenCalled();
|
||||
expect(setModalResetKeySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('collapses read-only sessions back to inventory and hides setup sections', () => {
|
||||
it('collapses read-only sessions back to the inventory and redirects', () => {
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
|
||||
expect(screen.queryByRole('button', { name: /Add infrastructure/i })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: 'Connections' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: 'Agent profiles' })).toBeNull();
|
||||
expect(screen.queryByTestId('platform-section')).toBeNull();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
expect(screen.queryByTestId('agent-profiles')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -779,7 +779,7 @@ describe('monitored-system model guardrails', () => {
|
|||
"navigate('/settings/infrastructure/proxmox')",
|
||||
);
|
||||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
'export function getInfrastructureWorkspaceViewFromPath',
|
||||
'export function deriveAddStepFromLegacyPath',
|
||||
);
|
||||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
"pathname.startsWith('/settings/infrastructure/proxmox')",
|
||||
|
|
|
|||
|
|
@ -510,9 +510,6 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(settingsInfrastructurePanelPropsSource).toContain('getInfrastructurePanelProps');
|
||||
expect(settingsNavigationModelSource).toContain('export type SettingsTab =');
|
||||
expect(settingsNavigationModelSource).toContain('export const DEFAULT_SETTINGS_TAB');
|
||||
expect(settingsNavigationModelSource).toContain(
|
||||
"const INFRASTRUCTURE_INSTALL_PREFIX = '/settings/infrastructure/install';",
|
||||
);
|
||||
expect(settingsNavigationModelSource).toContain('export function resolveCanonicalSettingsPath');
|
||||
expect(settingsNavigationModelSource).toContain('export function isProxmoxSettingsPath');
|
||||
expect(settingsNavigationModelSource).toContain('export function settingsTabPath');
|
||||
|
|
@ -525,7 +522,7 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(settingsNavigationHookSource).toContain('resolveCanonicalSelfHostedBillingHref');
|
||||
expect(settingsNavigationHookSource).toContain('settingsTabPath');
|
||||
expect(settingsNavigationHookSource).toContain('presentationPolicyIsReadOnly');
|
||||
expect(settingsNavigationHookSource).toContain("buildInfrastructureWorkspacePath('inventory')");
|
||||
expect(settingsNavigationHookSource).toContain('buildInfrastructureWorkspacePath');
|
||||
expect(settingsNavigationModelSource).toContain('hideFromSidebar?: boolean;');
|
||||
expect(settingsRoutingSource).toContain("from './settingsNavigationModel'");
|
||||
expect(settingsTypesSource).toContain("from './settingsNavigationModel'");
|
||||
|
|
@ -887,9 +884,7 @@ describe('Settings architecture guardrails', () => {
|
|||
'createSignal<InfrastructureWorkspaceView>',
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).toContain('createEffect(() =>');
|
||||
expect(infrastructureWorkspaceSource).toContain(
|
||||
"if (readOnlyWorkspace() && activeView() !== 'inventory')",
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).toContain('readOnly()');
|
||||
expect(infrastructureWorkspaceSource).toContain('./ConnectionsTable');
|
||||
expect(infrastructureWorkspaceSource).toContain('./InfrastructureActiveRowDetails');
|
||||
expect(infrastructureWorkspaceSource).toContain('./InfrastructureIgnoredRowDetails');
|
||||
|
|
@ -897,15 +892,12 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallerSection');
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureStopMonitoringDialog');
|
||||
expect(infrastructureWorkspaceSource).toContain('AgentProfilesPanel');
|
||||
expect(infrastructureWorkspaceSource).toContain('PlatformConnectionsWorkspace');
|
||||
expect(infrastructureWorkspaceSource).not.toContain(
|
||||
"import { Subtabs } from '@/components/shared/Subtabs'",
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).not.toContain('<Subtabs');
|
||||
expect(infrastructureWorkspaceSource).toContain('Platform connections');
|
||||
expect(infrastructureWorkspaceSource).toContain('Choose what to connect');
|
||||
expect(infrastructureWorkspaceSource).toContain('activeView() === \'platforms\'');
|
||||
expect(infrastructureWorkspaceSource).toContain('activeView() === \'install\'');
|
||||
expect(infrastructureWorkspaceSource).toContain("panelStep() === 'pick'");
|
||||
expect(infrastructureWorkspaceSource).toContain("panelStep() === 'agent'");
|
||||
expect(infrastructureWorkspaceSource).not.toContain("activeView() === 'operations'");
|
||||
expect(connectionsTableSource).toContain('headerActions');
|
||||
expect(connectionsTableModelSource).toContain('export function buildInfrastructureSystemRows');
|
||||
|
|
@ -964,7 +956,7 @@ describe('Settings architecture guardrails', () => {
|
|||
'export const INFRASTRUCTURE_WORKSPACE_TABS',
|
||||
);
|
||||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
'export function getInfrastructureWorkspaceViewFromPath',
|
||||
'export function deriveAddStepFromLegacyPath',
|
||||
);
|
||||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
'export function buildInfrastructureWorkspacePath',
|
||||
|
|
@ -1438,8 +1430,6 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(SETTINGS_HEADER_META['infrastructure-systems'].description).toBe(
|
||||
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
|
||||
);
|
||||
expect(SETTINGS_HEADER_META['infrastructure-connections'].title).toBe('Infrastructure');
|
||||
expect(SETTINGS_HEADER_META['infrastructure-install'].title).toBe('Infrastructure');
|
||||
});
|
||||
|
||||
it('keeps billing-related shell framing on self-hosted commercial terms', () => {
|
||||
|
|
@ -1489,13 +1479,13 @@ describe('Settings architecture guardrails', () => {
|
|||
|
||||
it('keeps read-only infrastructure landing and shell copy on the shared settings owner', () => {
|
||||
expect(settingsNavigationHookSource).toContain('presentationPolicyIsReadOnly');
|
||||
expect(settingsNavigationHookSource).toContain("buildInfrastructureWorkspacePath('inventory')");
|
||||
expect(settingsNavigationHookSource).toContain('buildInfrastructureWorkspacePath');
|
||||
expect(settingsShellStateSource).toContain('presentationPolicyIsReadOnly');
|
||||
expect(settingsShellStateSource).toContain(
|
||||
'Setup changes stay unavailable in this read-only session.',
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).toContain('presentationPolicyIsReadOnly');
|
||||
expect(infrastructureWorkspaceSource).toContain("activeView() !== 'inventory'");
|
||||
expect(infrastructureWorkspaceSource).toContain('readOnly()');
|
||||
});
|
||||
|
||||
it('keeps relay shell copy on the shared relay presentation owner', () => {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import { updateDisableLocalUpgradeMetricsSetting } from '@/stores/systemSettings
|
|||
|
||||
const canonicalTabPaths = {
|
||||
'infrastructure-systems': '/settings/infrastructure',
|
||||
'infrastructure-connections': '/settings/infrastructure/platforms',
|
||||
'infrastructure-install': '/settings/infrastructure/install',
|
||||
'system-general': '/settings/system-general',
|
||||
'system-network': '/settings/system-network',
|
||||
'system-updates': '/settings/system-updates',
|
||||
|
|
@ -309,7 +307,7 @@ describe('settingsNavigation integration scaffold', () => {
|
|||
});
|
||||
|
||||
it('keeps non-gated tabs unlocked', () => {
|
||||
expect(isTabLocked('infrastructure-connections', hasFeatures([]), () => true)).toBe(false);
|
||||
expect(isTabLocked('infrastructure-systems', hasFeatures([]), () => true)).toBe(false);
|
||||
});
|
||||
|
||||
it('getTabLockReason returns tier-specific reason for locked tabs and null for unlocked', () => {
|
||||
|
|
@ -325,7 +323,7 @@ describe('settingsNavigation integration scaffold', () => {
|
|||
);
|
||||
expect(getTabLockReason(tab, hasFeatures([requiredFeature]), () => true)).toBeNull();
|
||||
}
|
||||
expect(getTabLockReason('infrastructure-connections', hasFeatures([]), () => true)).toBeNull();
|
||||
expect(getTabLockReason('infrastructure-systems', hasFeatures([]), () => true)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import { isFeatureLocked, isTabLocked } from '../settingsFeatureGates';
|
|||
|
||||
const canonicalTabPaths = {
|
||||
'infrastructure-systems': '/settings/infrastructure',
|
||||
'infrastructure-connections': '/settings/infrastructure/platforms',
|
||||
'infrastructure-install': '/settings/infrastructure/install',
|
||||
'system-general': '/settings/system-general',
|
||||
'system-network': '/settings/system-network',
|
||||
'system-updates': '/settings/system-updates',
|
||||
|
|
@ -80,30 +78,29 @@ describe('settingsNavigationModel', () => {
|
|||
expect(resolveCanonicalSettingsPath('/settings/infrastructure')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
// All infrastructure sub-paths collapse to the single workspace.
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/install')).toBe(
|
||||
'/settings/infrastructure/install',
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/pve')).toBe(
|
||||
'/settings/infrastructure/platforms/proxmox/pve',
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api')).toBe(
|
||||
'/settings/infrastructure/platforms/proxmox',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api/pve')).toBe(
|
||||
'/settings/infrastructure/platforms/proxmox/pve',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/proxmox')).toBe(
|
||||
'/settings/infrastructure/platforms/proxmox',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/truenas')).toBe(
|
||||
'/settings/infrastructure/platforms/truenas',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/vmware')).toBe(
|
||||
'/settings/infrastructure/platforms/vmware',
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/platforms/truenas')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/operations')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
// Legacy proxmox paths redirect to the workspace.
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/api/pve')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/infrastructure/proxmox')).toBe(
|
||||
'/settings/infrastructure',
|
||||
);
|
||||
expect(resolveCanonicalSettingsPath('/settings/integrations/api')).toBe(
|
||||
'/settings/security/api',
|
||||
);
|
||||
|
|
@ -147,10 +144,10 @@ describe('settingsNavigationModel', () => {
|
|||
['?tab=infrastructure', 'infrastructure-systems'],
|
||||
['?tab=agents', 'infrastructure-systems'],
|
||||
['?tab=workloads', 'infrastructure-systems'],
|
||||
['?tab=proxmox', 'infrastructure-connections'],
|
||||
['?tab=connections', 'infrastructure-connections'],
|
||||
['?tab=platforms', 'infrastructure-connections'],
|
||||
['?tab=install', 'infrastructure-install'],
|
||||
['?tab=proxmox', 'infrastructure-systems'],
|
||||
['?tab=connections', 'infrastructure-systems'],
|
||||
['?tab=platforms', 'infrastructure-systems'],
|
||||
['?tab=install', 'infrastructure-systems'],
|
||||
['?tab=docker', 'infrastructure-systems'],
|
||||
['?tab=system-ai', 'system-ai'],
|
||||
['?tab=system-relay', 'system-relay'],
|
||||
|
|
@ -195,14 +192,16 @@ describe('settingsNavigationModel', () => {
|
|||
expect(isTabLocked(tab, hasFeatures([requiredFeature]), () => true)).toBe(false);
|
||||
expect(isTabLocked(tab, hasFeatures([]), () => false)).toBe(false);
|
||||
}
|
||||
expect(isTabLocked('infrastructure-connections', hasFeatures([]), () => true)).toBe(false);
|
||||
expect(isTabLocked('infrastructure-systems', hasFeatures([]), () => true)).toBe(false);
|
||||
});
|
||||
|
||||
it('maps deriveAgentFromPath contracts for canonical infrastructure routes', () => {
|
||||
const agentCases: Array<[string, 'pve' | 'pbs' | 'pmg' | null]> = [
|
||||
['/settings/infrastructure/api/pve', 'pve'],
|
||||
['/settings/infrastructure/api/pbs', 'pbs'],
|
||||
['/settings/infrastructure/api/pmg', 'pmg'],
|
||||
// Legacy api/* paths now resolve to the workspace base — no agent derived.
|
||||
['/settings/infrastructure/api/pve', null],
|
||||
['/settings/infrastructure/api/pbs', null],
|
||||
['/settings/infrastructure/api/pmg', null],
|
||||
// Canonical platform paths still resolve correctly.
|
||||
['/settings/infrastructure/platforms/proxmox/pve', 'pve'],
|
||||
['/settings/infrastructure/platforms/proxmox/pbs', 'pbs'],
|
||||
['/settings/infrastructure/platforms/proxmox/pmg', 'pmg'],
|
||||
|
|
@ -242,19 +241,15 @@ describe('settingsNavigationModel', () => {
|
|||
});
|
||||
|
||||
it('treats proxmox deep links as infrastructure aliases', () => {
|
||||
expect(deriveTabFromPath('/settings/infrastructure/platforms')).toBe(
|
||||
'infrastructure-connections',
|
||||
);
|
||||
expect(deriveTabFromPath('/settings/infrastructure/platforms')).toBe('infrastructure-systems');
|
||||
expect(deriveTabFromPath('/settings/infrastructure/platforms/proxmox')).toBe(
|
||||
'infrastructure-connections',
|
||||
'infrastructure-systems',
|
||||
);
|
||||
expect(deriveTabFromPath('/settings/infrastructure/platforms/proxmox/pve')).toBe(
|
||||
'infrastructure-connections',
|
||||
'infrastructure-systems',
|
||||
);
|
||||
expect(deriveTabFromPath('/settings/infrastructure/api/pve')).toBe(
|
||||
'infrastructure-connections',
|
||||
);
|
||||
expect(deriveTabFromPath('/settings/infrastructure/install')).toBe('infrastructure-install');
|
||||
expect(deriveTabFromPath('/settings/infrastructure/api/pve')).toBe('infrastructure-systems');
|
||||
expect(deriveTabFromPath('/settings/infrastructure/install')).toBe('infrastructure-systems');
|
||||
});
|
||||
|
||||
it('maps support and legacy operations routes back to support tabs', () => {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ function renderHarness(pathname = '/settings', search = '', hash = '') {
|
|||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => navigation.setActiveTab('infrastructure-connections')}>
|
||||
open connections settings
|
||||
<button type="button" onClick={() => navigation.setActiveTab('infrastructure-systems')}>
|
||||
open infrastructure settings
|
||||
</button>
|
||||
<div data-testid="selected-agent">{navigation.selectedAgent()}</div>
|
||||
</>
|
||||
|
|
@ -55,7 +55,7 @@ describe('useSettingsNavigation', () => {
|
|||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
renderHarness('/settings/system-general');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'open connections settings' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'open infrastructure settings' }));
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
|
||||
scroll: false,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
export type InfrastructureWorkspaceView = 'inventory' | 'install' | 'platforms';
|
||||
export type InfrastructureAddStep = 'agent' | 'pve' | 'pbs' | 'pmg' | 'truenas' | 'vmware';
|
||||
export type InfrastructurePanelStep = 'pick' | InfrastructureAddStep;
|
||||
|
||||
const INFRASTRUCTURE_WORKSPACE_PATHS: Record<InfrastructureWorkspaceView, string> = {
|
||||
inventory: '/settings/infrastructure',
|
||||
install: '/settings/infrastructure/install',
|
||||
platforms: '/settings/infrastructure/platforms',
|
||||
};
|
||||
const INFRASTRUCTURE_BASE_PATH = '/settings/infrastructure';
|
||||
|
||||
export function getInfrastructureWorkspaceViewFromPath(
|
||||
export function buildInfrastructureWorkspacePath(_view?: string): string {
|
||||
return INFRASTRUCTURE_BASE_PATH;
|
||||
}
|
||||
|
||||
export function deriveAddStepFromLegacyPath(
|
||||
pathname: string,
|
||||
): InfrastructureWorkspaceView {
|
||||
if (
|
||||
pathname.startsWith('/settings/infrastructure/platforms') ||
|
||||
pathname.startsWith('/settings/infrastructure/proxmox') ||
|
||||
pathname.startsWith('/settings/infrastructure/api') ||
|
||||
pathname.startsWith('/settings/infrastructure/truenas')
|
||||
) {
|
||||
return 'platforms';
|
||||
}
|
||||
if (pathname.startsWith('/settings/infrastructure/install')) {
|
||||
return 'install';
|
||||
}
|
||||
return 'inventory';
|
||||
}
|
||||
|
||||
export function buildInfrastructureWorkspacePath(view: InfrastructureWorkspaceView): string {
|
||||
return INFRASTRUCTURE_WORKSPACE_PATHS[view];
|
||||
): InfrastructurePanelStep | null {
|
||||
if (pathname.startsWith('/settings/infrastructure/install')) return 'agent';
|
||||
if (pathname.startsWith('/settings/infrastructure/platforms/proxmox/pbs')) return 'pbs';
|
||||
if (pathname.startsWith('/settings/infrastructure/platforms/proxmox/pmg')) return 'pmg';
|
||||
if (pathname.startsWith('/settings/infrastructure/platforms/proxmox')) return 'pve';
|
||||
if (pathname.startsWith('/settings/infrastructure/platforms/truenas')) return 'truenas';
|
||||
if (pathname.startsWith('/settings/infrastructure/platforms/vmware')) return 'vmware';
|
||||
if (pathname.startsWith('/settings/infrastructure/platforms')) return 'pick';
|
||||
if (pathname.startsWith('/settings/infrastructure/proxmox')) return 'pve';
|
||||
if (pathname.startsWith('/settings/infrastructure/truenas')) return 'truenas';
|
||||
if (pathname.startsWith('/settings/infrastructure/vmware')) return 'vmware';
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,7 @@ import {
|
|||
export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = {
|
||||
'infrastructure-systems': {
|
||||
title: 'Infrastructure',
|
||||
description:
|
||||
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
|
||||
},
|
||||
'infrastructure-connections': {
|
||||
title: 'Infrastructure',
|
||||
description:
|
||||
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
|
||||
},
|
||||
'infrastructure-install': {
|
||||
title: 'Infrastructure',
|
||||
description:
|
||||
`Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
|
||||
description: `Review monitored systems in one ledger, then use Add infrastructure when you need platform setup or agent install commands. ${SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral}`,
|
||||
},
|
||||
'system-general': {
|
||||
title: 'General',
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import Key from 'lucide-solid/icons/key';
|
|||
import Activity from 'lucide-solid/icons/activity';
|
||||
import Network from 'lucide-solid/icons/network';
|
||||
import Server from 'lucide-solid/icons/server';
|
||||
import PlugZap from 'lucide-solid/icons/plug-zap';
|
||||
import Download from 'lucide-solid/icons/download';
|
||||
import Users from 'lucide-solid/icons/users';
|
||||
import Sliders from 'lucide-solid/icons/sliders-horizontal';
|
||||
import RefreshCw from 'lucide-solid/icons/refresh-cw';
|
||||
|
|
@ -39,22 +37,6 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [
|
|||
icon: Server,
|
||||
iconProps: { strokeWidth: 2 },
|
||||
},
|
||||
{
|
||||
id: 'infrastructure-connections',
|
||||
label: 'Connections',
|
||||
icon: PlugZap,
|
||||
iconProps: { strokeWidth: 2 },
|
||||
hideFromSidebar: true,
|
||||
hideWhenReadOnly: true,
|
||||
},
|
||||
{
|
||||
id: 'infrastructure-install',
|
||||
label: 'Install',
|
||||
icon: Download,
|
||||
iconProps: { strokeWidth: 2 },
|
||||
hideFromSidebar: true,
|
||||
hideWhenReadOnly: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import {
|
|||
|
||||
export type SettingsTab =
|
||||
| 'infrastructure-systems'
|
||||
| 'infrastructure-connections'
|
||||
| 'infrastructure-install'
|
||||
| 'system-general'
|
||||
| 'system-network'
|
||||
| 'system-updates'
|
||||
|
|
@ -35,10 +33,7 @@ export type SettingsTab =
|
|||
| 'security-audit'
|
||||
| 'security-webhooks';
|
||||
|
||||
export type InfrastructureSettingsTab = Extract<
|
||||
SettingsTab,
|
||||
'infrastructure-systems' | 'infrastructure-connections' | 'infrastructure-install'
|
||||
>;
|
||||
export type InfrastructureSettingsTab = Extract<SettingsTab, 'infrastructure-systems'>;
|
||||
|
||||
export type AgentKey = 'pve' | 'pbs' | 'pmg';
|
||||
export type ProxmoxPlatformType = Extract<
|
||||
|
|
@ -92,19 +87,13 @@ export const DEFAULT_SETTINGS_TAB: SettingsTab = 'infrastructure-systems';
|
|||
const INFRASTRUCTURE_SYSTEMS_PREFIX = '/settings/infrastructure';
|
||||
const LEGACY_AGENTS_PREFIX = '/settings/workloads';
|
||||
const LEGACY_DOCKER_PREFIX = '/settings/workloads/docker';
|
||||
const INFRASTRUCTURE_INSTALL_PREFIX = '/settings/infrastructure/install';
|
||||
const INFRASTRUCTURE_OPERATIONS_PREFIX = '/settings/infrastructure/operations';
|
||||
const PLATFORM_CONNECTIONS_PREFIX = '/settings/infrastructure/platforms';
|
||||
const SUPPORT_PREFIX = '/settings/support';
|
||||
const SUPPORT_DIAGNOSTICS_PREFIX = `${SUPPORT_PREFIX}/diagnostics`;
|
||||
const SUPPORT_REPORTING_PREFIX = `${SUPPORT_PREFIX}/reporting`;
|
||||
const SUPPORT_LOGS_PREFIX = `${SUPPORT_PREFIX}/logs`;
|
||||
const TRUENAS_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/truenas`;
|
||||
const PROXMOX_PREFIX = `${PLATFORM_CONNECTIONS_PREFIX}/proxmox`;
|
||||
const PROXMOX_PREFIX = `${INFRASTRUCTURE_SYSTEMS_PREFIX}/platforms/proxmox`;
|
||||
const LEGACY_PROXMOX_PREFIX = '/settings/infrastructure/proxmox';
|
||||
const LEGACY_PROXMOX_API_PREFIX = '/settings/infrastructure/api';
|
||||
const LEGACY_TRUENAS_PREFIX = '/settings/infrastructure/truenas';
|
||||
const LEGACY_VMWARE_PREFIX = '/settings/infrastructure/vmware';
|
||||
const LEGACY_INTEGRATIONS_API_PREFIX = '/settings/integrations/api';
|
||||
const LEGACY_SETTINGS_OPERATIONS_PREFIX = '/settings/operations';
|
||||
const SECURITY_API_PREFIX = '/settings/security/api';
|
||||
|
|
@ -158,19 +147,14 @@ export function resolveCanonicalSettingsPath(path: string): string | null {
|
|||
if (normalizedPath === LEGACY_AGENTS_PREFIX) {
|
||||
return settingsTabPath(DEFAULT_SETTINGS_TAB);
|
||||
}
|
||||
if (normalizedPath === INFRASTRUCTURE_SYSTEMS_PREFIX) {
|
||||
if (normalizedPath === LEGACY_DOCKER_PREFIX) {
|
||||
return settingsTabPath(DEFAULT_SETTINGS_TAB);
|
||||
}
|
||||
if (normalizedPath === PLATFORM_CONNECTIONS_PREFIX) {
|
||||
return PLATFORM_CONNECTIONS_PREFIX;
|
||||
}
|
||||
if (normalizedPath === INFRASTRUCTURE_INSTALL_PREFIX) {
|
||||
return INFRASTRUCTURE_INSTALL_PREFIX;
|
||||
}
|
||||
if (normalizedPath === INFRASTRUCTURE_OPERATIONS_PREFIX) {
|
||||
// All infrastructure sub-paths collapse to the single infrastructure workspace.
|
||||
if (normalizedPath.startsWith(`${INFRASTRUCTURE_SYSTEMS_PREFIX}/`)) {
|
||||
return INFRASTRUCTURE_SYSTEMS_PREFIX;
|
||||
}
|
||||
if (normalizedPath === LEGACY_DOCKER_PREFIX) {
|
||||
if (normalizedPath === INFRASTRUCTURE_SYSTEMS_PREFIX) {
|
||||
return settingsTabPath(DEFAULT_SETTINGS_TAB);
|
||||
}
|
||||
if (normalizedPath === SUPPORT_PREFIX) {
|
||||
|
|
@ -182,44 +166,13 @@ export function resolveCanonicalSettingsPath(path: string): string | null {
|
|||
if (normalizedPath.startsWith(`${LEGACY_SETTINGS_OPERATIONS_PREFIX}/`)) {
|
||||
return buildLegacyOperationsSettingsPath(normalizedPath);
|
||||
}
|
||||
if (normalizedPath === LEGACY_PROXMOX_API_PREFIX) {
|
||||
return PROXMOX_PREFIX;
|
||||
}
|
||||
if (normalizedPath === LEGACY_PROXMOX_PREFIX) {
|
||||
return PROXMOX_PREFIX;
|
||||
}
|
||||
if (normalizedPath === LEGACY_TRUENAS_PREFIX) {
|
||||
return TRUENAS_PREFIX;
|
||||
}
|
||||
if (normalizedPath === LEGACY_VMWARE_PREFIX) {
|
||||
return `${PLATFORM_CONNECTIONS_PREFIX}/vmware`;
|
||||
}
|
||||
if (normalizedPath === `${INFRASTRUCTURE_SYSTEMS_PREFIX}/pve`) {
|
||||
return `${PROXMOX_PREFIX}/pve`;
|
||||
}
|
||||
if (normalizedPath === `${INFRASTRUCTURE_SYSTEMS_PREFIX}/pbs`) {
|
||||
return `${PROXMOX_PREFIX}/pbs`;
|
||||
}
|
||||
if (normalizedPath === `${INFRASTRUCTURE_SYSTEMS_PREFIX}/pmg`) {
|
||||
return `${PROXMOX_PREFIX}/pmg`;
|
||||
}
|
||||
if (normalizedPath === `${LEGACY_PROXMOX_API_PREFIX}/pve`) {
|
||||
return `${PROXMOX_PREFIX}/pve`;
|
||||
}
|
||||
if (normalizedPath === `${LEGACY_PROXMOX_API_PREFIX}/pbs`) {
|
||||
return `${PROXMOX_PREFIX}/pbs`;
|
||||
}
|
||||
if (normalizedPath === `${LEGACY_PROXMOX_API_PREFIX}/pmg`) {
|
||||
return `${PROXMOX_PREFIX}/pmg`;
|
||||
}
|
||||
if (normalizedPath === `${LEGACY_PROXMOX_PREFIX}/pve`) {
|
||||
return `${PROXMOX_PREFIX}/pve`;
|
||||
}
|
||||
if (normalizedPath === `${LEGACY_PROXMOX_PREFIX}/pbs`) {
|
||||
return `${PROXMOX_PREFIX}/pbs`;
|
||||
}
|
||||
if (normalizedPath === `${LEGACY_PROXMOX_PREFIX}/pmg`) {
|
||||
return `${PROXMOX_PREFIX}/pmg`;
|
||||
if (
|
||||
normalizedPath.startsWith(`${LEGACY_PROXMOX_API_PREFIX}/`) ||
|
||||
normalizedPath === LEGACY_PROXMOX_API_PREFIX ||
|
||||
normalizedPath.startsWith(`${LEGACY_PROXMOX_PREFIX}/`) ||
|
||||
normalizedPath === LEGACY_PROXMOX_PREFIX
|
||||
) {
|
||||
return INFRASTRUCTURE_SYSTEMS_PREFIX;
|
||||
}
|
||||
if (normalizedPath === LEGACY_INTEGRATIONS_API_PREFIX) {
|
||||
return SECURITY_API_PREFIX;
|
||||
|
|
@ -239,25 +192,12 @@ export function deriveTabFromPath(path: string): SettingsTab {
|
|||
if (canonicalPath === '/settings') return DEFAULT_SETTINGS_TAB;
|
||||
if (
|
||||
canonicalPath === INFRASTRUCTURE_SYSTEMS_PREFIX ||
|
||||
canonicalPath.startsWith(`${INFRASTRUCTURE_SYSTEMS_PREFIX}/`) ||
|
||||
canonicalPath === LEGACY_AGENTS_PREFIX ||
|
||||
canonicalPath === LEGACY_DOCKER_PREFIX
|
||||
) {
|
||||
return 'infrastructure-systems';
|
||||
}
|
||||
if (canonicalPath === INFRASTRUCTURE_INSTALL_PREFIX) {
|
||||
return 'infrastructure-install';
|
||||
}
|
||||
if (
|
||||
canonicalPath === PLATFORM_CONNECTIONS_PREFIX ||
|
||||
canonicalPath.startsWith(PROXMOX_PREFIX) ||
|
||||
canonicalPath.startsWith(TRUENAS_PREFIX) ||
|
||||
canonicalPath.startsWith(`${PLATFORM_CONNECTIONS_PREFIX}/vmware`) ||
|
||||
canonicalPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_PROXMOX_API_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_TRUENAS_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_VMWARE_PREFIX)
|
||||
)
|
||||
return 'infrastructure-connections';
|
||||
|
||||
if (canonicalPath.includes('/settings/system-general')) return 'system-general';
|
||||
if (canonicalPath.includes('/settings/system-network')) return 'system-network';
|
||||
|
|
@ -293,23 +233,21 @@ export function deriveTabFromPath(path: string): SettingsTab {
|
|||
}
|
||||
|
||||
export function deriveAgentFromPath(path: string): AgentKey | null {
|
||||
const canonicalPath = resolveCanonicalSettingsPath(path) ?? normalizeSettingsPath(path);
|
||||
|
||||
const normalizedPath = normalizeSettingsPath(path);
|
||||
for (const [agent, meta] of Object.entries(PROXMOX_AGENT_META) as Array<
|
||||
[AgentKey, (typeof PROXMOX_AGENT_META)[AgentKey]]
|
||||
>) {
|
||||
if (canonicalPath.includes(meta.path)) return agent;
|
||||
if (normalizedPath.includes(meta.path)) return agent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isProxmoxSettingsPath(path: string): boolean {
|
||||
const canonicalPath = resolveCanonicalSettingsPath(path) ?? normalizeSettingsPath(path);
|
||||
const normalizedPath = normalizeSettingsPath(path);
|
||||
return (
|
||||
canonicalPath === PLATFORM_CONNECTIONS_PREFIX ||
|
||||
canonicalPath.startsWith(PROXMOX_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_PROXMOX_API_PREFIX)
|
||||
normalizedPath.startsWith(PROXMOX_PREFIX) ||
|
||||
normalizedPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
|
||||
normalizedPath.startsWith(LEGACY_PROXMOX_API_PREFIX)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -356,9 +294,8 @@ export function deriveTabFromQuery(search: string): SettingsTab | null {
|
|||
case 'connections':
|
||||
case 'platforms':
|
||||
case 'proxmox':
|
||||
return 'infrastructure-connections';
|
||||
case 'install':
|
||||
return 'infrastructure-install';
|
||||
return 'infrastructure-systems';
|
||||
case 'system-recovery':
|
||||
return 'system-recovery';
|
||||
case 'system-updates':
|
||||
|
|
@ -420,10 +357,6 @@ export function settingsTabPath(tab: SettingsTab): string {
|
|||
switch (tab) {
|
||||
case 'infrastructure-systems':
|
||||
return INFRASTRUCTURE_SYSTEMS_PREFIX;
|
||||
case 'infrastructure-connections':
|
||||
return PLATFORM_CONNECTIONS_PREFIX;
|
||||
case 'infrastructure-install':
|
||||
return INFRASTRUCTURE_INSTALL_PREFIX;
|
||||
case 'system-recovery':
|
||||
return '/settings/system-recovery';
|
||||
case 'organization-overview':
|
||||
|
|
@ -454,11 +387,7 @@ export function settingsTabPath(tab: SettingsTab): string {
|
|||
}
|
||||
|
||||
export function isInfrastructureSettingsTab(tab: SettingsTab): tab is InfrastructureSettingsTab {
|
||||
return (
|
||||
tab === 'infrastructure-systems' ||
|
||||
tab === 'infrastructure-connections' ||
|
||||
tab === 'infrastructure-install'
|
||||
);
|
||||
return tab === 'infrastructure-systems';
|
||||
}
|
||||
|
||||
export function buildLegacyOperationsSettingsPath(path: string): string {
|
||||
|
|
|
|||
|
|
@ -51,14 +51,6 @@ export const createSettingsPanelRegistry = (
|
|||
component: SETTINGS_PANEL_REGISTRY_LOADERS.InfrastructureWorkspace,
|
||||
getProps: context.getInfrastructurePanelProps,
|
||||
},
|
||||
'infrastructure-connections': {
|
||||
component: SETTINGS_PANEL_REGISTRY_LOADERS.InfrastructureWorkspace,
|
||||
getProps: context.getInfrastructurePanelProps,
|
||||
},
|
||||
'infrastructure-install': {
|
||||
component: SETTINGS_PANEL_REGISTRY_LOADERS.InfrastructureWorkspace,
|
||||
getProps: context.getInfrastructurePanelProps,
|
||||
},
|
||||
'system-general': {
|
||||
component: context.systemGeneralPanel,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { logger } from '@/utils/logger';
|
|||
import { SettingsAPI } from '@/api/settings';
|
||||
import type { EventDataMap, EventType } from '@/stores/events';
|
||||
import type { NodeConfigWithStatus } from '@/types/nodes';
|
||||
import type { SettingsTab } from './settingsTypes';
|
||||
import {
|
||||
collectConfiguredInfrastructureHosts,
|
||||
type DiscoveryScanStatus,
|
||||
|
|
@ -37,7 +36,6 @@ type RawDiscoveredServer = {
|
|||
|
||||
interface UseInfrastructureDiscoveryRuntimeStateParams {
|
||||
eventBus: InfrastructureEventBus;
|
||||
currentTab: Accessor<SettingsTab>;
|
||||
nodes: Accessor<NodeConfigWithStatus[]>;
|
||||
discoveryEnabled: Accessor<boolean>;
|
||||
setDiscoveryEnabled: Setter<boolean>;
|
||||
|
|
@ -60,7 +58,6 @@ interface UseInfrastructureDiscoveryRuntimeStateParams {
|
|||
|
||||
export const useInfrastructureDiscoveryRuntimeState = ({
|
||||
eventBus,
|
||||
currentTab,
|
||||
nodes,
|
||||
discoveryEnabled,
|
||||
setDiscoveryEnabled,
|
||||
|
|
@ -492,11 +489,9 @@ export const useInfrastructureDiscoveryRuntimeState = ({
|
|||
discoveryInterval = undefined;
|
||||
}
|
||||
|
||||
if (currentTab() === 'infrastructure-connections') {
|
||||
discoveryInterval = setInterval(() => {
|
||||
void loadDiscoveredNodes();
|
||||
}, 30000);
|
||||
}
|
||||
discoveryInterval = setInterval(() => {
|
||||
void loadDiscoveredNodes();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
|
|
|
|||
|
|
@ -470,16 +470,12 @@ Pulse prepares the first-host install token from setup so you can move straight
|
|||
}));
|
||||
});
|
||||
|
||||
const openPlatformConnections = () => {
|
||||
navigate(buildInfrastructureWorkspacePath('platforms'));
|
||||
};
|
||||
|
||||
const openDashboard = () => {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
const openInfrastructureInventory = () => {
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'));
|
||||
navigate(buildInfrastructureWorkspacePath());
|
||||
};
|
||||
|
||||
const isEmbedded = () => options.embedded ?? false;
|
||||
|
|
@ -517,7 +513,6 @@ Pulse prepares the first-host install token from setup so you can move straight
|
|||
lookupValue,
|
||||
lookupWasAutoDetected,
|
||||
openDashboard,
|
||||
openPlatformConnections,
|
||||
openInfrastructureInventory,
|
||||
requiresToken,
|
||||
selectedAgentUrl,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Accessor, Setter, createEffect, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { logger } from '@/utils/logger';
|
||||
import type { EventDataMap, EventType } from '@/stores/events';
|
||||
import type { SettingsTab } from './settingsTypes';
|
||||
import { useInfrastructureConfiguredNodesState } from './useInfrastructureConfiguredNodesState';
|
||||
import { useInfrastructureDiscoveryRuntimeState } from './useInfrastructureDiscoveryRuntimeState';
|
||||
import { useTrueNASSettingsPanelState } from './useTrueNASSettingsPanelState';
|
||||
|
|
@ -19,7 +18,6 @@ type InfrastructureEventBus = {
|
|||
|
||||
interface UseInfrastructureSettingsStateParams {
|
||||
eventBus: InfrastructureEventBus;
|
||||
currentTab: Accessor<SettingsTab>;
|
||||
discoveryEnabled: Accessor<boolean>;
|
||||
setDiscoveryEnabled: Setter<boolean>;
|
||||
discoverySubnet: Accessor<string>;
|
||||
|
|
@ -46,7 +44,6 @@ interface UseInfrastructureSettingsStateParams {
|
|||
|
||||
export function useInfrastructureSettingsState({
|
||||
eventBus,
|
||||
currentTab,
|
||||
discoveryEnabled,
|
||||
setDiscoveryEnabled,
|
||||
discoverySubnet,
|
||||
|
|
@ -82,7 +79,6 @@ export function useInfrastructureSettingsState({
|
|||
|
||||
const discoveryRuntime = useInfrastructureDiscoveryRuntimeState({
|
||||
eventBus,
|
||||
currentTab,
|
||||
nodes: configuredNodes.nodes,
|
||||
discoveryEnabled,
|
||||
setDiscoveryEnabled,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@ import {
|
|||
deriveAgentFromPath,
|
||||
deriveTabFromPath,
|
||||
deriveTabFromQuery,
|
||||
isInfrastructureSettingsTab,
|
||||
isProxmoxSettingsPath,
|
||||
resolveCanonicalSettingsPath,
|
||||
settingsAgentPath,
|
||||
settingsTabPath,
|
||||
type AgentKey,
|
||||
type SettingsTab,
|
||||
|
|
@ -39,33 +37,16 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
|
|||
const [currentTab, setCurrentTab] = createSignal<SettingsTab>(
|
||||
deriveTabFromPath(location.pathname),
|
||||
);
|
||||
const resolveTabPath = (tab: SettingsTab): string =>
|
||||
presentationPolicyIsReadOnly() &&
|
||||
isInfrastructureSettingsTab(tab) &&
|
||||
tab !== 'infrastructure-systems'
|
||||
? buildInfrastructureWorkspacePath('inventory')
|
||||
: settingsTabPath(tab);
|
||||
const resolveTabPath = (tab: SettingsTab): string => settingsTabPath(tab);
|
||||
const activeTab = (): SettingsTab => currentTab();
|
||||
|
||||
const [selectedAgent, setSelectedAgent] = createSignal<AgentKey>('pve');
|
||||
|
||||
const handleSelectAgent = (agent: AgentKey) => {
|
||||
setSelectedAgent(agent);
|
||||
if (currentTab() !== 'infrastructure-connections') {
|
||||
setCurrentTab('infrastructure-connections');
|
||||
}
|
||||
const target = settingsAgentPath(agent);
|
||||
if (target && location.pathname !== target) {
|
||||
navigate(target, { scroll: false });
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveTab = (tab: SettingsTab) => {
|
||||
if (tab === 'infrastructure-connections' && deriveAgentFromPath(location.pathname) === null) {
|
||||
setSelectedAgent('pve');
|
||||
}
|
||||
// Eagerly update tab state so UI reflects the click immediately,
|
||||
// even before the URL change triggers the sync effect.
|
||||
if (currentTab() !== tab) {
|
||||
setCurrentTab(tab);
|
||||
}
|
||||
|
|
@ -75,7 +56,6 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
|
|||
}
|
||||
};
|
||||
|
||||
// Keep tab state in sync with canonical URLs, while preserving old deep links as aliases.
|
||||
createEffect(
|
||||
on(
|
||||
() => [location.pathname, location.search, location.hash] as const,
|
||||
|
|
@ -108,23 +88,26 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
|
|||
|
||||
if (
|
||||
presentationPolicyIsReadOnly() &&
|
||||
(path === '/settings/infrastructure' ||
|
||||
path.startsWith('/settings/infrastructure/platforms') ||
|
||||
path.startsWith('/settings/infrastructure/install') ||
|
||||
path.startsWith('/settings/infrastructure/proxmox') ||
|
||||
path.startsWith('/settings/infrastructure/api') ||
|
||||
path.startsWith('/settings/infrastructure/truenas') ||
|
||||
path.startsWith('/settings/infrastructure/vmware') ||
|
||||
(path.startsWith('/settings/infrastructure') ||
|
||||
path === '/settings/workloads' ||
|
||||
path === '/settings/workloads/docker')
|
||||
) {
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'), {
|
||||
navigate(buildInfrastructureWorkspacePath(), {
|
||||
replace: true,
|
||||
scroll: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync Proxmox agent from the raw path before any redirect so deep
|
||||
// links like /platforms/proxmox/pbs still seed the correct agent.
|
||||
if (isProxmoxSettingsPath(path)) {
|
||||
const agentFromPath = deriveAgentFromPath(path) ?? 'pve';
|
||||
if (selectedAgent() !== agentFromPath) {
|
||||
setSelectedAgent(agentFromPath);
|
||||
}
|
||||
}
|
||||
|
||||
const canonicalPath = resolveCanonicalSettingsPath(path);
|
||||
if (canonicalPath && canonicalPath !== path) {
|
||||
navigate(canonicalPath, {
|
||||
|
|
@ -139,13 +122,6 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
|
|||
if (resolved !== currentTab()) {
|
||||
setCurrentTab(resolved);
|
||||
}
|
||||
|
||||
if (isProxmoxSettingsPath(effectivePath)) {
|
||||
const agentFromPath = deriveAgentFromPath(effectivePath) ?? 'pve';
|
||||
if (selectedAgent() !== agentFromPath) {
|
||||
setSelectedAgent(agentFromPath);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,8 +54,7 @@ const UNIFIED_RESOURCE_GUIDANCE = {
|
|||
],
|
||||
} as const;
|
||||
|
||||
const INFRASTRUCTURE_INSTALL_PATH = '/settings/infrastructure/install';
|
||||
const INFRASTRUCTURE_PLATFORMS_PATH = '/settings/infrastructure/platforms';
|
||||
const INFRASTRUCTURE_PATH = '/settings/infrastructure';
|
||||
const SETUP_WIZARD_TELEMETRY_SURFACE = 'setup_wizard_complete';
|
||||
|
||||
export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
|
||||
|
|
@ -141,8 +140,7 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
|
|||
|
||||
const downloadCredentials = () => {
|
||||
const baseUrl = getPulseBaseUrl();
|
||||
const installWorkspaceUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_INSTALL_PATH}`;
|
||||
const platformConnectionsUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_PLATFORMS_PATH}`;
|
||||
const infrastructureUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_PATH}`;
|
||||
const content = `Pulse Credentials
|
||||
==================
|
||||
Generated: ${new Date().toISOString()}
|
||||
|
|
@ -157,23 +155,12 @@ Admin API Token:
|
|||
----------------
|
||||
${props.state.apiToken}
|
||||
|
||||
Infrastructure Install Workspace:
|
||||
---------------------------------
|
||||
${installWorkspaceUrl}
|
||||
Infrastructure:
|
||||
---------------
|
||||
${infrastructureUrl}
|
||||
|
||||
Platform Connections Workspace:
|
||||
-------------------------------
|
||||
${platformConnectionsUrl}
|
||||
|
||||
Use the Infrastructure Install workspace to:
|
||||
- continue with the first-host install token Pulse prepares from setup
|
||||
- choose the agent connection URL
|
||||
- configure TLS and custom CA options
|
||||
- copy Linux, macOS, Windows, and related install commands
|
||||
|
||||
Use the Platform connections workspace when:
|
||||
- the first system is API-backed, such as Proxmox, TrueNAS, or VMware
|
||||
- Pulse should poll that platform directly instead of starting with a host install
|
||||
Use Add infrastructure to connect your first host or API-backed platform
|
||||
(Proxmox, TrueNAS, VMware, and others).
|
||||
|
||||
Keep these credentials secure!
|
||||
`;
|
||||
|
|
@ -190,11 +177,11 @@ Keep these credentials secure!
|
|||
};
|
||||
|
||||
const handleOpenInstallWorkspace = () => {
|
||||
props.onComplete(INFRASTRUCTURE_INSTALL_PATH);
|
||||
props.onComplete(INFRASTRUCTURE_PATH);
|
||||
};
|
||||
|
||||
const handleOpenPlatformConnections = () => {
|
||||
props.onComplete(INFRASTRUCTURE_PLATFORMS_PATH);
|
||||
props.onComplete(INFRASTRUCTURE_PATH);
|
||||
};
|
||||
|
||||
const handleGoToDashboard = () => {
|
||||
|
|
@ -414,10 +401,10 @@ Keep these credentials secure!
|
|||
|
||||
<div class="bg-surface-hover dark:bg-black border border-border rounded-md p-3">
|
||||
<div class="text-[11px] font-medium text-muted mb-1 uppercase tracking-wider">
|
||||
Infrastructure Install Workspace
|
||||
Infrastructure
|
||||
</div>
|
||||
<code class="text-base-content font-mono text-xs break-all">
|
||||
{INFRASTRUCTURE_INSTALL_PATH}
|
||||
{INFRASTRUCTURE_PATH}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,25 +5,16 @@ import setupCompletionModelSource from '../setupCompletionModel.ts?raw';
|
|||
describe('SetupCompletionPanel guardrails', () => {
|
||||
it('keeps setup completion aligned with the canonical infrastructure install workspace', () => {
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
"const INFRASTRUCTURE_INSTALL_PATH = '/settings/infrastructure/install';",
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
"const INFRASTRUCTURE_PLATFORMS_PATH = '/settings/infrastructure/platforms';",
|
||||
"const INFRASTRUCTURE_PATH = '/settings/infrastructure';",
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('Open Infrastructure Install');
|
||||
expect(setupCompletionPanelSource).toContain('Open Platform connections');
|
||||
expect(setupCompletionPanelSource).toContain('Infrastructure Install Workspace');
|
||||
expect(setupCompletionPanelSource).toContain('Platform Connections Workspace');
|
||||
expect(setupCompletionPanelSource).toContain('Credentials you must save now');
|
||||
expect(setupCompletionPanelSource).toContain('Shown during setup');
|
||||
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_INSTALL_PATH);');
|
||||
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_PLATFORMS_PATH);');
|
||||
expect(setupCompletionPanelSource).toContain('Use the Infrastructure Install workspace to:');
|
||||
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_PATH);');
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'continue with the first-host install token Pulse prepares from setup',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('Use the Platform connections workspace when:');
|
||||
expect(setupCompletionPanelSource).toContain('configure TLS and custom CA options');
|
||||
expect(setupCompletionPanelSource).not.toContain("from '@/stores/licenseCommercial';");
|
||||
expect(setupCompletionPanelSource).not.toContain('runStartProTrialAction');
|
||||
expect(setupCompletionPanelSource).not.toContain('loadCommercialPosture');
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ describe('SetupCompletionPanel', () => {
|
|||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Open Infrastructure Install' })[0]);
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/install');
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
|
||||
it('hands API-backed starts into the canonical platform connections workspace', async () => {
|
||||
|
|
@ -128,7 +128,7 @@ describe('SetupCompletionPanel', () => {
|
|||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/platforms');
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
|
||||
it('downloads credentials that point operators to the install workspace instead of inline commands', async () => {
|
||||
|
|
@ -155,16 +155,9 @@ describe('SetupCompletionPanel', () => {
|
|||
const content = await getLastDownloadedBlob().text();
|
||||
expect(content).toContain('Web Login:');
|
||||
expect(content).toContain('Admin API Token:');
|
||||
expect(content).toContain('Infrastructure Install Workspace:');
|
||||
expect(content).toContain('https://pulse.example.com/settings/infrastructure/install');
|
||||
expect(content).toContain('Platform Connections Workspace:');
|
||||
expect(content).toContain('https://pulse.example.com/settings/infrastructure/platforms');
|
||||
expect(content).toContain(
|
||||
'continue with the first-host install token Pulse prepares from setup',
|
||||
);
|
||||
expect(content).toContain(
|
||||
'the first system is API-backed, such as Proxmox, TrueNAS, or VMware',
|
||||
);
|
||||
expect(content).toContain('Infrastructure:');
|
||||
expect(content).toContain('https://pulse.example.com/settings/infrastructure');
|
||||
expect(content).toContain('Use Add infrastructure to connect');
|
||||
expect(content).not.toContain('Example Install Command');
|
||||
expect(content).not.toContain('Example Windows Install Command');
|
||||
|
||||
|
|
@ -291,7 +284,7 @@ describe('SetupCompletionPanel', () => {
|
|||
name: 'Open Platform connections',
|
||||
}),
|
||||
);
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/platforms');
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
|
||||
it('derives admitted VMware onboarding from the governed platform manifest', async () => {
|
||||
|
|
@ -331,7 +324,7 @@ describe('SetupCompletionPanel', () => {
|
|||
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure/platforms');
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
|
||||
it('keeps both continuation paths visible when install-managed and API-backed systems are already present', async () => {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('SetupCompletionPreview', () => {
|
|||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Open Infrastructure Install' })[0]);
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/install');
|
||||
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
|
||||
it('renders the VMware-connected preview scenario without polling runtime state', () => {
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ describe('Dashboard page module contract', () => {
|
|||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open infrastructure install' }));
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
|
||||
it('renders the governed storage and recovery dashboard panels', () => {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,6 @@ describe('Infrastructure empty state', () => {
|
|||
|
||||
fireEvent.click(getByText('Open Infrastructure Install'));
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('buildStorageRecoveryTabSpecs', () => {
|
|||
expect(specs.map((spec) => spec.label)).toEqual(['Storage', 'Recovery']);
|
||||
expect(specs.map((spec) => spec.route)).toEqual([buildStoragePath(), buildRecoveryPath()]);
|
||||
expect(specs.map((spec) => spec.settingsRoute)).toEqual([
|
||||
'/settings/infrastructure/platforms/proxmox/pbs',
|
||||
'/settings/infrastructure',
|
||||
'/settings/system-recovery',
|
||||
]);
|
||||
expect(specs.every((spec) => spec.badge === undefined)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function buildStorageRecoveryTabSpecs(): StorageRecoveryTabSpec[] {
|
|||
id: 'storage',
|
||||
label: 'Storage',
|
||||
route: buildStoragePath(),
|
||||
settingsRoute: '/settings/infrastructure/platforms/proxmox/pbs',
|
||||
settingsRoute: '/settings/infrastructure',
|
||||
tooltip: 'Storage pools, disks, and datastores',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3425,7 +3425,7 @@ describe('frontend resource type boundaries', () => {
|
|||
);
|
||||
expect(infrastructureWorkspaceSource).not.toContain('inferViewFromPath');
|
||||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
'export function getInfrastructureWorkspaceViewFromPath',
|
||||
'export function deriveAddStepFromLegacyPath',
|
||||
);
|
||||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
'export function buildInfrastructureWorkspacePath',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue