Files
ProxmoxVE-Local/src/app/_components/ExecutionModeModal.tsx
Michel Roegl-Brunner 7b8c1ebdf1 feat: Add default and advanced install method selection
- Add ConfigurationModal component for selecting default or advanced installation mode
- Default mode: Uses predefined defaults with minimal user input (hostname from slug, vmbr0, dhcp, etc.)
- Advanced mode: Full configuration modal with all environment variables customizable
- Add support for IPv4 CIDR input when network mode is 'static'
- Add support for IPv6 static address input when IPv6 method is 'static'
- Implement password formatting as '-password <password>' for build.func compatibility
- Auto-enable SSH when password or SSH keys are provided
- Add storage selection dropdowns filtered by server node assignment
- Pass environment variables through entire execution stack (frontend -> WebSocket -> SSH/local execution)
- Add mode environment variable (always set to 'default' for script execution)
- Update ExecutionModeModal to show 'Advanced (Beta)' option
2025-12-05 15:53:50 +01:00

297 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import type { Server } from '../../types/server';
import type { Script } from '../../types/script';
import { Button } from './ui/button';
import { ColorCodedDropdown } from './ColorCodedDropdown';
import { SettingsModal } from './SettingsModal';
import { ConfigurationModal, type EnvVars } from './ConfigurationModal';
import { useRegisterModal } from './modal/ModalStackProvider';
interface ExecutionModeModalProps {
isOpen: boolean;
onClose: () => void;
onExecute: (mode: 'local' | 'ssh', server?: Server, envVars?: EnvVars) => void;
scriptName: string;
script?: Script | null;
}
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName, script }: ExecutionModeModalProps) {
useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
const [servers, setServers] = useState<Server[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [configModalOpen, setConfigModalOpen] = useState(false);
const [configMode, setConfigMode] = useState<'default' | 'advanced'>('default');
useEffect(() => {
if (isOpen) {
void fetchServers();
}
}, [isOpen]);
// Auto-select server when exactly one server is available
useEffect(() => {
if (isOpen && !loading && servers.length === 1) {
setSelectedServer(servers[0] ?? null);
}
}, [isOpen, loading, servers]);
// Refresh servers when settings modal closes
const handleSettingsModalClose = () => {
setSettingsModalOpen(false);
// Refetch servers to reflect any changes made in settings
void fetchServers();
};
const fetchServers = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/servers');
if (!response.ok) {
throw new Error('Failed to fetch servers');
}
const data = await response.json();
// Sort servers by name alphabetically
const sortedServers = (data as Server[]).sort((a, b) =>
(a.name ?? '').localeCompare(b.name ?? '')
);
setServers(sortedServers);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
const handleConfigModeSelect = (mode: 'default' | 'advanced') => {
if (!selectedServer) {
setError('Please select a server first');
return;
}
setConfigMode(mode);
setConfigModalOpen(true);
};
const handleConfigConfirm = (envVars: EnvVars) => {
if (!selectedServer) return;
setConfigModalOpen(false);
onExecute('ssh', selectedServer, envVars);
onClose();
};
const handleServerSelect = (server: Server | null) => {
setSelectedServer(server);
setError(null); // Clear error when server is selected
};
if (!isOpen) return null;
return (
<>
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border">
<h2 className="text-xl font-bold text-foreground">Select Server</h2>
<Button
onClick={onClose}
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</Button>
</div>
{/* Content */}
<div className="p-6">
{error && (
<div className="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-destructive" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-destructive">{error}</p>
</div>
</div>
</div>
)}
{loading ? (
<div className="text-center py-8">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<p className="mt-2 text-sm text-muted-foreground">Loading servers...</p>
</div>
) : servers.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm">No servers configured</p>
<p className="text-xs mt-1">Add servers in Settings to execute scripts</p>
<Button
onClick={() => setSettingsModalOpen(true)}
variant="outline"
size="sm"
className="mt-3"
>
Open Server Settings
</Button>
</div>
) : servers.length === 1 ? (
/* Single Server Confirmation View */
<div className="space-y-6">
<div className="text-center">
<h3 className="text-lg font-medium text-foreground mb-2">
Install Script Confirmation
</h3>
<p className="text-sm text-muted-foreground">
Do you want to install &quot;{scriptName}&quot; on the following server?
</p>
</div>
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
<div className="w-3 h-3 bg-success rounded-full"></div>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{selectedServer?.name ?? 'Unnamed Server'}
</p>
<p className="text-sm text-muted-foreground">
{selectedServer?.ip}
</p>
</div>
</div>
</div>
{/* Configuration Mode Selection */}
<div className="space-y-3">
<p className="text-sm text-muted-foreground text-center">
Choose configuration mode:
</p>
<div className="flex gap-3">
<Button
onClick={() => handleConfigModeSelect('default')}
variant="default"
size="default"
className="flex-1"
>
Default
</Button>
<Button
onClick={() => handleConfigModeSelect('advanced')}
variant="outline"
size="default"
className="flex-1"
>
Advanced (Beta)
</Button>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end space-x-3">
<Button
onClick={onClose}
variant="outline"
size="default"
>
Cancel
</Button>
</div>
</div>
) : (
/* Multiple Servers Selection View */
<div className="space-y-6">
<div className="mb-6">
<h3 className="text-lg font-medium text-foreground mb-2">
Select server to execute &quot;{scriptName}&quot;
</h3>
</div>
{/* Server Selection */}
<div className="mb-6">
<label htmlFor="server" className="block text-sm font-medium text-foreground mb-2">
Select Server
</label>
<ColorCodedDropdown
servers={servers}
selectedServer={selectedServer}
onServerSelect={handleServerSelect}
placeholder="Select a server..."
/>
</div>
{/* Configuration Mode Selection - only show when server is selected */}
{selectedServer && (
<div className="space-y-3 pt-4 border-t border-border">
<p className="text-sm text-muted-foreground text-center">
Choose configuration mode:
</p>
<div className="flex gap-3">
<Button
onClick={() => handleConfigModeSelect('default')}
variant="default"
size="default"
className="flex-1"
>
Default
</Button>
<Button
onClick={() => handleConfigModeSelect('advanced')}
variant="outline"
size="default"
className="flex-1"
>
Advanced
</Button>
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex justify-end space-x-3">
<Button
onClick={onClose}
variant="outline"
size="default"
>
Cancel
</Button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Server Settings Modal */}
<SettingsModal
isOpen={settingsModalOpen}
onClose={handleSettingsModalClose}
/>
{/* Configuration Modal */}
<ConfigurationModal
isOpen={configModalOpen}
onClose={() => setConfigModalOpen(false)}
onConfirm={handleConfigConfirm}
script={script ?? null}
server={selectedServer}
mode={configMode}
/>
</>
);
}