fix: use toasts instead of inline banners for Storage panel feedback

Replaces the layout-shifting error banner at the bottom of the panel
with non-intrusive toast notifications. Upload limit save, cleanup
success, and all error feedback now use the existing toast system.
Cleanup preview results stay inline as expected contextual responses.
This commit is contained in:
Jannis Braun
2026-03-23 02:32:57 +01:00
parent ed854cce53
commit 9044bfd26f
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import { api } from '../../../api/client'; import { api } from '../../../api/client';
import type { StorageStats, CleanupResult } from '@backspace/shared'; import type { StorageStats, CleanupResult } from '@backspace/shared';
import { useSettingsStore } from '../../../stores/settingsStore'; import { useSettingsStore } from '../../../stores/settingsStore';
import { useUIStore } from '../../../stores/uiStore';
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
@@ -14,7 +15,8 @@ function formatBytes(bytes: number): string {
export function StoragePanel() { export function StoragePanel() {
const [stats, setStats] = useState<StorageStats | null>(null); const [stats, setStats] = useState<StorageStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [loadError, setLoadError] = useState('');
const addToast = useUIStore((s) => s.addToast);
const [cleanupResult, setCleanupResult] = useState<CleanupResult | null>(null); const [cleanupResult, setCleanupResult] = useState<CleanupResult | null>(null);
const [cleaning, setCleaning] = useState(false); const [cleaning, setCleaning] = useState(false);
const [previewDone, setPreviewDone] = useState(false); const [previewDone, setPreviewDone] = useState(false);
@@ -34,12 +36,12 @@ export function StoragePanel() {
const fetchStats = useCallback(async () => { const fetchStats = useCallback(async () => {
setLoading(true); setLoading(true);
setError(''); setLoadError('');
try { try {
const data = await api.admin.storageStats(); const data = await api.admin.storageStats();
setStats(data); setStats(data);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load storage stats'); setLoadError(err instanceof Error ? err.message : 'Failed to load storage stats');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -60,8 +62,9 @@ export function StoragePanel() {
try { try {
await updateInstanceSettings({ maxUploadSizeMb: uploadLimitMb }); await updateInstanceSettings({ maxUploadSizeMb: uploadLimitMb });
setUploadLimitDirty(false); setUploadLimitDirty(false);
addToast(`Upload limit set to ${uploadLimitMb} MB`, 'success');
} catch { } catch {
setError('Failed to update upload limit'); addToast('Failed to update upload limit', 'warning');
} finally { } finally {
setUploadLimitSaving(false); setUploadLimitSaving(false);
} }
@@ -70,7 +73,6 @@ export function StoragePanel() {
const handleMediaCleanup = async (dryRun: boolean) => { const handleMediaCleanup = async (dryRun: boolean) => {
setMediaCleaning(true); setMediaCleaning(true);
setMediaCleanupResult(null); setMediaCleanupResult(null);
setError('');
try { try {
const result = await api.admin.cleanupOldMedia(mediaAgeDays, dryRun); const result = await api.admin.cleanupOldMedia(mediaAgeDays, dryRun);
setMediaCleanupResult(result); setMediaCleanupResult(result);
@@ -78,10 +80,11 @@ export function StoragePanel() {
setMediaPreviewDone(true); setMediaPreviewDone(true);
} else { } else {
setMediaPreviewDone(false); setMediaPreviewDone(false);
addToast(`Deleted ${result.deletedFiles} file${result.deletedFiles !== 1 ? 's' : ''} (${formatBytes(result.freedBytes)})`, 'success');
await fetchStats(); await fetchStats();
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Media cleanup failed'); addToast(err instanceof Error ? err.message : 'Media cleanup failed', 'warning');
} finally { } finally {
setMediaCleaning(false); setMediaCleaning(false);
} }
@@ -90,7 +93,6 @@ export function StoragePanel() {
const handleCleanup = async (dryRun: boolean) => { const handleCleanup = async (dryRun: boolean) => {
setCleaning(true); setCleaning(true);
setCleanupResult(null); setCleanupResult(null);
setError('');
try { try {
const result = await api.admin.storageCleanup(dryRun); const result = await api.admin.storageCleanup(dryRun);
setCleanupResult(result); setCleanupResult(result);
@@ -98,10 +100,11 @@ export function StoragePanel() {
setPreviewDone(true); setPreviewDone(true);
} else { } else {
setPreviewDone(false); setPreviewDone(false);
addToast(`Cleaned up ${result.deletedFiles} file${result.deletedFiles !== 1 ? 's' : ''} (${formatBytes(result.freedBytes)})`, 'success');
await fetchStats(); await fetchStats();
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Cleanup failed'); addToast(err instanceof Error ? err.message : 'Cleanup failed', 'warning');
} finally { } finally {
setCleaning(false); setCleaning(false);
} }
@@ -111,10 +114,10 @@ export function StoragePanel() {
return <div className="text-sm text-txt-tertiary">Loading storage stats...</div>; return <div className="text-sm text-txt-tertiary">Loading storage stats...</div>;
} }
if (error && !stats) { if (loadError && !stats) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div> <div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{loadError}</div>
<button onClick={fetchStats} className="text-sm text-accent-primary hover:underline">Retry</button> <button onClick={fetchStats} className="text-sm text-accent-primary hover:underline">Retry</button>
</div> </div>
); );
@@ -321,11 +324,6 @@ export function StoragePanel() {
</div> </div>
</div> </div>
{/* Error / Refresh */}
{error && (
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
)}
<button <button
onClick={() => { setCleanupResult(null); setPreviewDone(false); fetchStats(); }} onClick={() => { setCleanupResult(null); setPreviewDone(false); fetchStats(); }}
className="text-sm text-accent-primary hover:underline" className="text-sm text-accent-primary hover:underline"