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 | |