feat: tree view for cached objects, top-files stats on dashboard (#48)
- Objects page renders paths as a collapsible tree instead of flat list with expand/collapse all, aggregated size/hits per directory - Dashboard gains top-files-by-hits and top-files-by-bandwidth tables - Backend: new /api/v2/stats/top-files-by-hits and /api/v2/stats/top-files-by-bandwidth endpoints - Raised per_page max to 5000 for objects listing --------- Co-authored-by: Ben Vincent <ben@unkin.net> Reviewed-on: #48
This commit was merged in pull request #48.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
import type { OverviewStats, RemoteStatRow, HealthStatus } from '../api/types';
|
||||
import type { OverviewStats, RemoteStatRow, FileStatRow, BandwidthStatRow, HealthStatus } from '../api/types';
|
||||
import { StatsCard } from '../components/StatsCard';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { DataTable } from '../components/DataTable';
|
||||
@@ -11,14 +11,24 @@ import './Dashboard.css';
|
||||
export function Dashboard() {
|
||||
const [stats, setStats] = useState<OverviewStats | null>(null);
|
||||
const [topRemotes, setTopRemotes] = useState<RemoteStatRow[]>([]);
|
||||
const [topFilesByHits, setTopFilesByHits] = useState<FileStatRow[]>([]);
|
||||
const [topFilesByBW, setTopFilesByBW] = useState<BandwidthStatRow[]>([]);
|
||||
const [health, setHealth] = useState<HealthStatus | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([api.stats(), api.topRemotes(), api.health()])
|
||||
.then(([s, tr, h]) => {
|
||||
Promise.all([
|
||||
api.stats(),
|
||||
api.topRemotes(),
|
||||
api.topFilesByHits(),
|
||||
api.topFilesByBandwidth(),
|
||||
api.health(),
|
||||
])
|
||||
.then(([s, tr, tfh, tfb, h]) => {
|
||||
setStats(s);
|
||||
setTopRemotes(tr || []);
|
||||
setTopFilesByHits(tfh || []);
|
||||
setTopFilesByBW(tfb || []);
|
||||
setHealth(h);
|
||||
})
|
||||
.catch(e => setError(e.message));
|
||||
@@ -87,6 +97,72 @@ export function Dashboard() {
|
||||
data={topRemotes}
|
||||
emptyMessage="No remotes configured yet"
|
||||
/>
|
||||
|
||||
<div className="top-files-grid">
|
||||
<div>
|
||||
<h2 className="section-title">Top Files by Hits</h2>
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
key: 'path',
|
||||
header: 'File',
|
||||
render: (r: FileStatRow) => (
|
||||
<Link to={`/remotes/${r.remote_name}/objects`} className="file-link">
|
||||
<span className="file-remote">{r.remote_name}</span>
|
||||
<span className="file-path mono">{r.path}</span>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'hits',
|
||||
header: 'Hits',
|
||||
render: (r: FileStatRow) => formatNumber(r.access_count),
|
||||
width: '90px',
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
header: 'Size',
|
||||
render: (r: FileStatRow) => formatBytes(r.size_bytes),
|
||||
width: '100px',
|
||||
},
|
||||
]}
|
||||
data={topFilesByHits}
|
||||
emptyMessage="No cached files yet"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="section-title">Top Files by Bandwidth (30d)</h2>
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
key: 'path',
|
||||
header: 'File',
|
||||
render: (r: BandwidthStatRow) => (
|
||||
<Link to={`/remotes/${r.remote_name}/objects`} className="file-link">
|
||||
<span className="file-remote">{r.remote_name}</span>
|
||||
<span className="file-path mono">{r.path}</span>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bandwidth',
|
||||
header: 'Bandwidth',
|
||||
render: (r: BandwidthStatRow) => formatBytes(r.bandwidth),
|
||||
width: '110px',
|
||||
},
|
||||
{
|
||||
key: 'requests',
|
||||
header: 'Requests',
|
||||
render: (r: BandwidthStatRow) => formatNumber(r.requests),
|
||||
width: '100px',
|
||||
},
|
||||
]}
|
||||
data={topFilesByBW}
|
||||
emptyMessage="No access data yet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user