Spaces:
Runtime error
Runtime error
import React, { useState, useEffect } from 'react'; | |
import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; | |
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; | |
const StatsVisualization = () => { | |
const [stats, setStats] = useState(null); | |
const [storageStats, setStorageStats] = useState(null); | |
useEffect(() => { | |
fetchStats(); | |
const interval = setInterval(fetchStats, 30000); | |
return () => clearInterval(interval); | |
}, []); | |
const fetchStats = async () => { | |
try { | |
const [statsResponse, storageResponse] = await Promise.all([ | |
fetch('/backend/image-stats'), | |
fetch('/backend/storage-stats') | |
]); | |
const statsData = await statsResponse.json(); | |
const storageData = await storageResponse.json(); | |
setStats(statsData); | |
setStorageStats(storageData); | |
} catch (error) { | |
console.error('Fehler beim Laden der Statistiken:', error); | |
} | |
}; | |
if (!stats || !storageStats) return <div>Lade Statistiken...</div>; | |
return ( | |
<div className="container-fluid p-4"> | |
<div className="row g-4"> | |
{/* Monatliche Bilderzahl */} | |
<div className="col-12"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Bilder pro Monat</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<LineChart data={stats.monthly}> | |
<CartesianGrid strokeDasharray="3 3" /> | |
<XAxis dataKey="month" /> | |
<YAxis /> | |
<Tooltip /> | |
<Legend /> | |
<Line | |
type="monotone" | |
dataKey="count" | |
stroke="#8884d8" | |
name="Anzahl Bilder" | |
/> | |
</LineChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* Album & Kategorie Verteilung */} | |
<div className="col-md-6"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Alben Verteilung</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<PieChart> | |
<Pie | |
data={[ | |
{ name: 'In Album', value: stats.albums.with }, | |
{ name: 'Ohne Album', value: stats.albums.without } | |
]} | |
cx="50%" | |
cy="50%" | |
labelLine={false} | |
label={({name, percent}) => `${name}: ${(percent * 100).toFixed(0)}%`} | |
outerRadius={80} | |
fill="#8884d8" | |
dataKey="value" | |
> | |
{stats.albums.map((entry, index) => ( | |
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> | |
))} | |
</Pie> | |
<Tooltip /> | |
<Legend /> | |
</PieChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="col-md-6"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Kategorie Verteilung</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<PieChart> | |
<Pie | |
data={[ | |
{ name: 'Mit Kategorie', value: stats.categories.with }, | |
{ name: 'Ohne Kategorie', value: stats.categories.without } | |
]} | |
cx="50%" | |
cy="50%" | |
labelLine={false} | |
label={({name, percent}) => `${name}: ${(percent * 100).toFixed(0)}%`} | |
outerRadius={80} | |
fill="#8884d8" | |
dataKey="value" | |
> | |
{stats.categories.map((entry, index) => ( | |
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> | |
))} | |
</Pie> | |
<Tooltip /> | |
<Legend /> | |
</PieChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* Speichernutzung nach Format */} | |
<div className="col-12"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Speichernutzung nach Format</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<BarChart data={storageStats}> | |
<CartesianGrid strokeDasharray="3 3" /> | |
<XAxis dataKey="format" /> | |
<YAxis yAxisId="left" orientation="left" stroke="#8884d8" /> | |
<YAxis yAxisId="right" orientation="right" stroke="#82ca9d" /> | |
<Tooltip /> | |
<Legend /> | |
<Bar yAxisId="left" dataKey="count" name="Anzahl" fill="#8884d8" /> | |
<Bar yAxisId="right" dataKey="sizeMB" name="Größe (MB)" fill="#82ca9d" /> | |
</BarChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default StatsVisualization; | |
### END: stats-visualization.txt | |
### START: stats-visualization-updated.txt | |
import React, { useState, useEffect, useCallback } from 'react'; | |
import { API, APIHandler } from '@/api'; | |
import { | |
LineChart, Line, BarChart, Bar, PieChart, Pie, | |
XAxis, YAxis, CartesianGrid, Tooltip, Legend, | |
ResponsiveContainer, Cell | |
} from 'recharts'; | |
// Farbpalette für Charts | |
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d']; | |
const StatsVisualization = () => { | |
const [stats, setStats] = useState(null); | |
const [storageStats, setStorageStats] = useState(null); | |
const [loading, setLoading] = useState(true); | |
const [error, setError] = useState(null); | |
const [refreshInterval, setRefreshInterval] = useState(30000); // 30 Sekunden | |
// Daten laden | |
const fetchStats = useCallback(async () => { | |
try { | |
setLoading(true); | |
const [statsData, storageData] = await Promise.all([ | |
APIHandler.get(API.statistics.images), | |
APIHandler.get(API.statistics.storage) | |
]); | |
setStats(statsData); | |
setStorageStats(storageData); | |
setError(null); | |
} catch (err) { | |
setError('Fehler beim Laden der Statistiken: ' + err.message); | |
console.error('Fetch error:', err); | |
} finally { | |
setLoading(false); | |
} | |
}, []); | |
// Auto-Refresh | |
useEffect(() => { | |
fetchStats(); | |
if (refreshInterval > 0) { | |
const interval = setInterval(fetchStats, refreshInterval); | |
return () => clearInterval(interval); | |
} | |
}, [fetchStats, refreshInterval]); | |
// Format für Byte-Größen | |
const formatBytes = (bytes) => { | |
if (bytes === 0) return '0 B'; | |
const k = 1024; | |
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
}; | |
// Custom Tooltip für Charts | |
const CustomTooltip = ({ active, payload, label, valueFormatter }) => { | |
if (!active || !payload || !payload.length) return null; | |
return ( | |
<div className="custom-tooltip bg-white p-3 rounded shadow"> | |
<p className="label mb-2">{`${label}`}</p> | |
{payload.map((entry, index) => ( | |
<p key={index} style={{ color: entry.color }} className="mb-1"> | |
{`${entry.name}: ${valueFormatter ? valueFormatter(entry.value) : entry.value}`} | |
</p> | |
))} | |
</div> | |
); | |
}; | |
if (loading) { | |
return ( | |
<div className="d-flex justify-content-center p-5"> | |
<div className="spinner-border text-primary" role="status"> | |
<span className="visually-hidden">Laden...</span> | |
</div> | |
</div> | |
); | |
} | |
if (error) { | |
return ( | |
<div className="alert alert-danger m-3" role="alert"> | |
<h4 className="alert-heading">Fehler</h4> | |
<p>{error}</p> | |
<button | |
className="btn btn-outline-danger" | |
onClick={fetchStats} | |
> | |
Erneut versuchen | |
</button> | |
</div> | |
); | |
} | |
return ( | |
<div className="container-fluid p-4"> | |
{/* Toolbar */} | |
<div className="d-flex justify-content-between align-items-center mb-4"> | |
<div className="btn-group"> | |
<button | |
className="btn btn-primary" | |
onClick={fetchStats} | |
> | |
<i className="bi bi-arrow-clockwise"></i> Aktualisieren | |
</button> | |
<select | |
className="form-select" | |
value={refreshInterval} | |
onChange={(e) => setRefreshInterval(Number(e.target.value))} | |
> | |
<option value="0">Kein Auto-Refresh</option> | |
<option value="15000">Alle 15 Sekunden</option> | |
<option value="30000">Alle 30 Sekunden</option> | |
<option value="60000">Jede Minute</option> | |
</select> | |
</div> | |
</div> | |
<div className="row g-4"> | |
{/* Monatliche Bilder */} | |
<div className="col-12"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Bilder pro Monat</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<LineChart data={stats?.monthly}> | |
<CartesianGrid strokeDasharray="3 3" /> | |
<XAxis dataKey="month" /> | |
<YAxis /> | |
<Tooltip content={<CustomTooltip />} /> | |
<Legend /> | |
<Line | |
type="monotone" | |
dataKey="count" | |
name="Anzahl Bilder" | |
stroke={COLORS[0]} | |
strokeWidth={2} | |
dot={{ r: 4 }} | |
activeDot={{ r: 6 }} | |
/> | |
</LineChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* Album & Kategorie Verteilung */} | |
<div className="col-md-6"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Album Verteilung</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<PieChart> | |
<Pie | |
data={[ | |
{ name: 'In Alben', value: stats?.albums.with }, | |
{ name: 'Ohne Album', value: stats?.albums.without } | |
]} | |
cx="50%" | |
cy="50%" | |
labelLine={false} | |
label={({name, percent}) => | |
`${name}: ${(percent * 100).toFixed(1)}%` | |
} | |
outerRadius={80} | |
fill="#8884d8" | |
dataKey="value" | |
> | |
{stats?.albums.map((entry, index) => ( | |
<Cell | |
key={`cell-${index}`} | |
fill={COLORS[index % COLORS.length]} | |
/> | |
))} | |
</Pie> | |
<Tooltip content={<CustomTooltip />} /> | |
<Legend /> | |
</PieChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="col-md-6"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Kategorie Verteilung</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<PieChart> | |
<Pie | |
data={[ | |
{ name: 'Mit Kategorie', value: stats?.categories.with }, | |
{ name: 'Ohne Kategorie', value: stats?.categories.without } | |
]} | |
cx="50%" | |
cy="50%" | |
labelLine={false} | |
label={({name, percent}) => | |
`${name}: ${(percent * 100).toFixed(1)}%` | |
} | |
outerRadius={80} | |
fill="#8884d8" | |
dataKey="value" | |
> | |
{stats?.categories.map((entry, index) => ( | |
<Cell | |
key={`cell-${index}`} | |
fill={COLORS[index % COLORS.length]} | |
/> | |
))} | |
</Pie> | |
<Tooltip content={<CustomTooltip />} /> | |
<Legend /> | |
</PieChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* Speichernutzung */} | |
<div className="col-12"> | |
<div className="card"> | |
<div className="card-body"> | |
<h5 className="card-title">Speichernutzung nach Format</h5> | |
<div style={{ height: '300px' }}> | |
<ResponsiveContainer width="100%" height="100%"> | |
<BarChart data={storageStats}> | |
<CartesianGrid strokeDasharray="3 3" /> | |
<XAxis dataKey="format" /> | |
<YAxis | |
yAxisId="left" | |
orientation="left" | |
stroke={COLORS[0]} | |
/> | |
<YAxis | |
yAxisId="right" | |
orientation="right" | |
stroke={COLORS[1]} | |
/> | |
<Tooltip content={<CustomTooltip valueFormatter={formatBytes} />} /> | |
<Legend /> | |
<Bar | |
yAxisId="left" | |
dataKey="count" | |
name="Anzahl" | |
fill={COLORS[0]} | |
/> | |
<Bar | |
yAxisId="right" | |
dataKey="size" | |
name="Größe" | |
fill={COLORS[1]} | |
/> | |
</BarChart> | |
</ResponsiveContainer> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default StatsVisualization; | |
window.StatsVisualization = StatsVisualization; // <-- Diese Zeile ans Ende setzen | |