wrolpi/app/src/components/Apps.js
lrnselfreliance 306386e801
app: Flasher saved configs and Statistics distinguish pending from empty (#733)
The Flasher's saved configurations started as [] with no loading
flag, and the Saved Firmwares tab is the default tab, so every load
painted "No saved configurations yet." until the fetch returned.  The
api helper returned [] on a non-OK response too, so a failed fetch
read as an empty list.

useStatistics used useState() with no initial value, so the
Statistics page's first render saw undefined and flashed "Failed to
fetch statistics" for one frame before the effect set {} and the
spinner appeared.

Invariants now enforced and tested:
* null = pending, undefined = fetch failed, [] / object = loaded.  The
  api helper returns undefined (and toasts) on a non-OK response.
* The Saved Firmwares tab shows a placeholder while pending, an error
  on failure, and "No saved configurations yet." only once the server
  confirmed there are none.  Only the newest configs request may write
  state, since save and delete refetch.
* The Statistics page shows its loader while pending and its error
  only for a failed fetch.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 17:05:02 -06:00

112 lines
4 KiB
JavaScript

import React from 'react';
import {Link, Route, Routes} from "react-router";
import {
ErrorMessage,
humanFileSize,
humanNumber,
mimetypeColor,
PageContainer,
toLocaleString,
useTitle
} from "./Common";
import {Header, Loading, Panel, Statistic, StatisticGroup} from "./ui";
import {useStatistics} from "../hooks/customHooks";
import {CalculatorsPage} from "./Calculators";
import {BookmarksPage} from "./Bookmarks";
// `statistics` follows useStatistics: null = pending, undefined = fetch failed.
export function StatisticsPage() {
useTitle('Statistics');
const {statistics} = useStatistics();
if (statistics === undefined) {
return <>
<Header as='h1'>Statistics</Header>
<ErrorMessage>Failed to fetch statistics</ErrorMessage>
</>
}
if (statistics && statistics['global_statistics']) {
let {global_statistics, file_statistics} = statistics;
const {
archive_count,
audio_count,
ebook_count,
image_count,
pdf_count,
total_count,
video_count,
zip_count,
total_size,
tagged_files,
tagged_zims,
tags_count,
} = file_statistics;
const {db_size} = global_statistics;
return <>
<Header as='h1'>Statistics</Header>
<Header as='h2'>Files</Header>
<Panel>
<StatisticGroup>
<Statistic value={toLocaleString(total_count)} label='All Files'/>
<Statistic value={humanFileSize(total_size)} label='Total Size'/>
</StatisticGroup>
</Panel>
<Panel>
<StatisticGroup>
<Link to={'/videos/statistics'}>
<Statistic color={mimetypeColor('video/')} value={toLocaleString(video_count)}
label='Videos'/>
</Link>
<Statistic color={mimetypeColor('application/pdf')} value={toLocaleString(pdf_count)}
label='PDFs'/>
<Statistic color={mimetypeColor('application/epub')} value={toLocaleString(ebook_count)}
label='eBooks'/>
<Statistic color={mimetypeColor('text/html')} value={toLocaleString(archive_count)}
label='Archives'/>
<Statistic color={mimetypeColor('image/')} value={toLocaleString(image_count)}
label='Images'/>
</StatisticGroup>
</Panel>
<Panel>
<StatisticGroup>
<Statistic value={toLocaleString(zip_count)} label='ZIP'/>
<Statistic color={mimetypeColor('audio/')} value={toLocaleString(audio_count)} label='Audio'/>
</StatisticGroup>
</Panel>
<Header as='h2'>Tags</Header>
<Panel>
<StatisticGroup>
<Statistic value={humanNumber(tags_count)} label='Tags'/>
<Statistic value={humanNumber(tagged_files)} label='Tagged Files'/>
<Statistic value={humanNumber(tagged_zims)} label='Tagged Zims'/>
</StatisticGroup>
</Panel>
<Header as='h2'>Database</Header>
<Panel>
<StatisticGroup>
<Statistic value={humanFileSize(db_size)} label='Size'/>
</StatisticGroup>
</Panel>
</>;
}
return <>
<Header as='h1'>Statistics</Header>
<Panel><Loading/></Panel>
</>;
}
export function MoreRoute(props) {
return <PageContainer>
<Routes>
<Route path='calculators' element={<CalculatorsPage/>}/>
<Route path='statistics' exact element={<StatisticsPage/>}/>
<Route path='bookmarks' element={<BookmarksPage/>}/>
</Routes>
</PageContainer>
}