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:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user