Skip to content

Commit 08b7eec

Browse files
feat: implement disk resize with improved modal flow and error handling (#177)
- Add automatic disk resizing when changing LXC disk size in settings - Implement separate modal flow: confirmation -> loading -> result -> settings - Add proper error detection for pct resize command (check both exit code and output) - Add LVM fallback resize method when pct resize fails - Implement configuration rollback on resize failure - Update modal styling to use semantic color classes for proper dark mode support - Add data refresh after result modal close to show updated values - Remove success/error banners from settings modal for cleaner UI
1 parent c1b478e commit 08b7eec

File tree

2 files changed

+490
-34
lines changed

2 files changed

+490
-34
lines changed

src/app/_components/LXCSettingsModal.tsx

Lines changed: 133 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ interface LXCSettingsModalProps {
4343
export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSettingsModalProps) {
4444
const [activeTab, setActiveTab] = useState<string>('common');
4545
const [showConfirmation, setShowConfirmation] = useState(false);
46+
const [showResultModal, setShowResultModal] = useState(false);
47+
const [resultType, setResultType] = useState<'success' | 'error' | null>(null);
48+
const [resultMessage, setResultMessage] = useState<string | null>(null);
4649
const [error, setError] = useState<string | null>(null);
47-
const [successMessage, setSuccessMessage] = useState<string | null>(null);
4850
const [hasChanges, setHasChanges] = useState(false);
4951
const [forceSync] = useState(false);
52+
const [isSaving, setIsSaving] = useState(false);
5053

5154
const [formData, setFormData] = useState<any>({
5255
arch: '',
@@ -76,27 +79,41 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
7679
});
7780

7881
// tRPC hooks
79-
const { data: configData, isLoading } = api.installedScripts.getLXCConfig.useQuery(
82+
const { data: configData, isLoading, refetch } = api.installedScripts.getLXCConfig.useQuery(
8083
{ scriptId: script?.id ?? 0, forceSync },
8184
{ enabled: !!script && isOpen }
8285
);
8386

8487
const saveMutation = api.installedScripts.saveLXCConfig.useMutation({
85-
onSuccess: () => {
86-
setSuccessMessage('LXC configuration saved successfully');
87-
setHasChanges(false);
88+
onSuccess: (data) => {
89+
console.log('Save mutation success data:', data);
90+
setIsSaving(false);
8891
setShowConfirmation(false);
89-
onSave();
92+
93+
if (data.success) {
94+
setResultType('success');
95+
setResultMessage(data.message ?? 'LXC configuration saved successfully');
96+
setHasChanges(false);
97+
} else {
98+
console.log('Backend returned error:', data.error);
99+
setResultType('error');
100+
setResultMessage(data.error ?? 'Failed to save configuration');
101+
}
102+
setShowResultModal(true);
90103
},
91104
onError: (err) => {
92-
setError(`Failed to save configuration: ${err.message}`);
105+
console.log('Save mutation error:', err);
106+
setIsSaving(false);
107+
setShowConfirmation(false);
108+
setResultType('error');
109+
setResultMessage(`Failed to save configuration: ${err.message}`);
110+
setShowResultModal(true);
93111
}
94112
});
95113

96114
const syncMutation = api.installedScripts.syncLXCConfig.useMutation({
97115
onSuccess: (result) => {
98116
populateFormData(result);
99-
setSuccessMessage('Configuration synced from server successfully');
100117
setHasChanges(false);
101118
},
102119
onError: (err) => {
@@ -158,13 +175,61 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
158175
syncMutation.mutate({ scriptId: script.id });
159176
};
160177

178+
const validateForm = () => {
179+
// Check required fields
180+
if (!formData.arch?.trim()) {
181+
setError('Architecture is required');
182+
return false;
183+
}
184+
if (!formData.cores || formData.cores < 1) {
185+
setError('Cores must be at least 1');
186+
return false;
187+
}
188+
if (!formData.memory || formData.memory < 128) {
189+
setError('Memory must be at least 128 MB');
190+
return false;
191+
}
192+
if (!formData.hostname?.trim()) {
193+
setError('Hostname is required');
194+
return false;
195+
}
196+
if (!formData.ostype?.trim()) {
197+
setError('OS Type is required');
198+
return false;
199+
}
200+
if (!formData.rootfs_storage?.trim()) {
201+
setError('Root filesystem storage is required');
202+
return false;
203+
}
204+
205+
// Check if trying to decrease disk size
206+
const currentSize = configData?.config?.rootfs_size ?? '0G';
207+
const newSize = formData.rootfs_size ?? '0G';
208+
const currentSizeGB = parseFloat(String(currentSize));
209+
const newSizeGB = parseFloat(String(newSize));
210+
211+
if (newSizeGB < currentSizeGB) {
212+
setError('Disk size cannot be decreased. Only increases are allowed for safety.');
213+
return false;
214+
}
215+
216+
return true;
217+
};
218+
161219
const handleSave = () => {
162-
setShowConfirmation(true);
220+
setError(null);
221+
222+
// Validate form - only show confirmation modal if no errors
223+
if (validateForm()) {
224+
setShowConfirmation(true);
225+
}
163226
};
164227

165228
const handleConfirmSave = () => {
166229
if (!script) return;
167230
setError(null);
231+
setIsSaving(true);
232+
setShowConfirmation(false);
168233

169234
saveMutation.mutate({
170235
scriptId: script.id,
@@ -179,6 +244,14 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
179244
});
180245
};
181246

247+
const handleResultModalClose = () => {
248+
setShowResultModal(false);
249+
setResultType(null);
250+
setResultMessage(null);
251+
// Refresh the data to show updated values
252+
void refetch();
253+
};
254+
182255
if (!isOpen || !script) return null;
183256

184257
return (
@@ -229,23 +302,6 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
229302
</div>
230303
)}
231304

232-
{/* Success Message */}
233-
{successMessage && (
234-
<div className="bg-green-50 dark:bg-green-950/20 border-b border-green-200 dark:border-green-800 p-4">
235-
<div className="flex items-start gap-3">
236-
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-500 flex-shrink-0 mt-0.5" />
237-
<div className="flex-1">
238-
<p className="text-sm font-medium text-green-800 dark:text-green-200">{successMessage}</p>
239-
</div>
240-
<button
241-
onClick={() => setSuccessMessage(null)}
242-
className="text-green-600 dark:text-green-500 hover:text-green-700 dark:hover:text-green-400"
243-
>
244-
245-
</button>
246-
</div>
247-
</div>
248-
)}
249305

250306
{/* Error Message */}
251307
{error && (
@@ -485,13 +541,19 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
485541
/>
486542
</div>
487543
<div className="space-y-2">
488-
<label htmlFor="rootfs_size" className="block text-sm font-medium text-foreground">Size</label>
544+
<label htmlFor="rootfs_size" className="block text-sm font-medium text-foreground">
545+
Size
546+
<span className="text-xs text-muted-foreground ml-2">(can only be increased)</span>
547+
</label>
489548
<Input
490549
id="rootfs_size"
491550
value={formData.rootfs_size}
492551
onChange={(e) => handleInputChange('rootfs_size', e.target.value)}
493552
placeholder="4G"
494553
/>
554+
<p className="text-xs text-muted-foreground">
555+
Disk size can only be increased for safety. Format: 4G, 8G, 16G, etc.
556+
</p>
495557
</div>
496558
</div>
497559
</div>
@@ -590,10 +652,10 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
590652
</Button>
591653
<Button
592654
onClick={handleSave}
593-
disabled={saveMutation.isPending || !hasChanges}
655+
disabled={isSaving || saveMutation.isPending || !hasChanges}
594656
variant="default"
595657
>
596-
{saveMutation.isPending ? 'Saving...' : 'Save Configuration'}
658+
{isSaving ? 'Saving & Resizing...' : saveMutation.isPending ? 'Saving...' : 'Save Configuration'}
597659
</Button>
598660
</div>
599661
</div>
@@ -608,17 +670,55 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
608670
}}
609671
onConfirm={handleConfirmSave}
610672
title="Confirm LXC Configuration Changes"
611-
message="Modifying LXC configuration can break your container and may require manual recovery. Ensure you understand these changes before proceeding. The container may need to be restarted for changes to take effect."
673+
message={`Modifying LXC configuration can break your container and may require manual recovery. Ensure you understand these changes before proceeding.${
674+
formData.rootfs_size && configData?.config?.rootfs_size &&
675+
parseFloat(String(formData.rootfs_size)) > parseFloat(String(configData.config.rootfs_size ?? '0'))
676+
? `\n\n⚠️ DISK RESIZE DETECTED: The disk size will be increased from ${configData.config.rootfs_size} to ${formData.rootfs_size}. This operation will automatically resize the underlying storage and filesystem.`
677+
: ''
678+
}\n\nThe container may need to be restarted for changes to take effect.`}
612679
variant="danger"
613680
confirmText={script.container_id ?? ''}
614-
confirmButtonText="Save Configuration"
681+
confirmButtonText={formData.rootfs_size && configData?.config?.rootfs_size &&
682+
parseFloat(String(formData.rootfs_size)) > parseFloat(String(configData.config.rootfs_size ?? '0'))
683+
? "Save & Resize Disk" : "Save Configuration"}
615684
/>
616685

617686
{/* Loading Modal */}
618687
<LoadingModal
619-
isOpen={isLoading}
620-
action="Loading LXC configuration..."
688+
isOpen={isLoading || isSaving}
689+
action={isSaving ? "Saving configuration and resizing disk..." : "Loading LXC configuration..."}
621690
/>
691+
692+
{/* Result Modal */}
693+
{showResultModal && resultType && resultMessage && (
694+
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
695+
<div className="bg-card text-card-foreground rounded-lg shadow-xl max-w-md w-full mx-4 border border-border">
696+
<div className="p-6">
697+
<div className="flex items-center gap-3 mb-4">
698+
{resultType === 'success' ? (
699+
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-500" />
700+
) : (
701+
<AlertTriangle className="h-6 w-6 text-red-600 dark:text-red-500" />
702+
)}
703+
<h3 className="text-lg font-semibold text-card-foreground">
704+
{resultType === 'success' ? 'Success' : 'Error'}
705+
</h3>
706+
</div>
707+
<p className="text-muted-foreground mb-6">
708+
{resultMessage}
709+
</p>
710+
<div className="flex justify-end">
711+
<button
712+
onClick={handleResultModalClose}
713+
className="px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 rounded-md transition-colors"
714+
>
715+
Close
716+
</button>
717+
</div>
718+
</div>
719+
</div>
720+
</div>
721+
)}
622722
</>
623723
);
624724
}

0 commit comments

Comments
 (0)