chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { InfoIcon } from 'lucide-react';
import { AppTile } from '#/components/atoms/app-tile.jsx';
import { Card } from '#/components/atoms/card.jsx';
import { Chip } from '#/components/atoms/chip.jsx';
import { Dialog } from '#/components/molecules/dialog.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { StorageBar } from '#/components/molecules/storage-bar.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { post } from '#/lib/api.js';
import { KIND_META, KIND_ORDER } from '#/lib/file-kinds.js';
import { bytes, num } from '#/lib/format.js';
import { useApiMutation } from '#/lib/query.js';
import { cn } from '#/lib/utils.js';
/** The Newest/Largest toggle with its how-it-works explainer. */
function SortControls({ sort, filters, hrefFor, onNavigate }) {
return (
<>
{['newest', 'largest'].map((order) => (
<Chip
key={order}
active={sort === order}
href={hrefFor({
...filters,
sort: order === 'largest' ? 'largest' : undefined,
})}
onNavigate={onNavigate}
>
{order === 'newest' ? 'Newest' : 'Largest'}
</Chip>
))}
<Dialog
title="How files are ordered"
trigger={
<button
type="button"
aria-label="How files are ordered"
className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-full text-faint outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<InfoIcon className="size-4" />
</button>
}
>
<div className="space-y-3 text-[13px] text-muted-foreground leading-relaxed">
<p>
<span className="font-semibold text-foreground">Newest</span> lists
files by when their content was made. The date comes from the record
that uses the file, so it stays accurate even if your account moves
between hosts.
</p>
<p>
Some records carry no date of their own. Those files are listed by
when they reached this server instead, and their dates read
“Stored”.
</p>
<p>
<span className="font-semibold text-foreground">Largest</span> lists
files by size, biggest first.
</p>
</div>
</Dialog>
</>
);
}
/** The bar plus every filter control; the chip rows double as the legend. */
export function FilesHeader({ facets, filters, hrefFor, onNavigate }) {
const sort = filters.sort === 'largest' ? 'largest' : 'newest';
const kinds = KIND_ORDER.map((kind) =>
facets.kinds.find((facet) => facet.kind === kind),
).filter(Boolean);
// Rebuilds blob usage from record content, for repos that arrived by
// import, where usage undercounts until the links are rebuilt.
const relink = useApiMutation([['blobs']]);
return (
<Card className="mb-4 p-5">
<div className="flex items-baseline justify-between gap-3">
<p className="text-[12.5px] text-muted-foreground">
{num(facets.total.count)}{' '}
{facets.total.count === 1 ? 'file' : 'files'}, using{' '}
{bytes(facets.total.bytes)} of storage.
</p>
<TextLink
className="text-[12px]"
onClick={() => relink.run(() => post('/blobs/relink'))}
disabled={relink.busy}
title="Re-read every record to rebuild which files are used. Useful after moving to this server."
>
{relink.busy ? 'Rechecking…' : 'Recheck usage'}
</TextLink>
</div>
<ErrorNotice error={relink.error} />
<StorageBar
facets={facets}
filters={filters}
hrefFor={hrefFor}
onNavigate={onNavigate}
/>
<div className="-mx-5 -mb-1 mt-3.5 flex items-center gap-1.5 overflow-x-auto px-5 pb-1 [-ms-overflow-style:none] [scrollbar-width:none] sm:mx-0 sm:mb-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0 [&::-webkit-scrollbar]:hidden">
{/* On a phone this row scrolls sideways, so sort leads at the left
edge, always in view; the kinds trail off, hinting at the scroll.
On wider screens sort moves to its usual place at the right. */}
<div className="flex shrink-0 items-center gap-1.5 sm:hidden">
<SortControls
sort={sort}
filters={filters}
hrefFor={hrefFor}
onNavigate={onNavigate}
/>
<span aria-hidden className="mx-0.5 h-4 w-px shrink-0 bg-border" />
</div>
{kinds.map((facet) => (
<Chip
key={facet.kind}
active={filters.kind === facet.kind}
href={hrefFor({
...filters,
kind: filters.kind === facet.kind ? undefined : facet.kind,
})}
onNavigate={onNavigate}
title={bytes(facet.bytes)}
>
<span
className={cn('size-2 rounded-full', KIND_META[facet.kind].dot)}
/>
{KIND_META[facet.kind].label}
<span className="font-normal text-faint">{num(facet.count)}</span>
</Chip>
))}
{facets.unused.count > 0 && (
<Chip
active={Boolean(filters.unused)}
href={hrefFor({ ...filters, unused: !filters.unused })}
onNavigate={onNavigate}
title={bytes(facets.unused.bytes)}
>
Not used
<span className="font-normal text-faint">
{num(facets.unused.count)}
</span>
</Chip>
)}
<div className="ml-auto hidden items-center gap-1.5 sm:flex">
<SortControls
sort={sort}
filters={filters}
hrefFor={hrefFor}
onNavigate={onNavigate}
/>
</div>
</div>
{facets.apps.length > 0 && (
<div className="-mx-5 -mb-1 mt-2 flex items-center gap-1.5 overflow-x-auto px-5 pb-1 [-ms-overflow-style:none] [scrollbar-width:none] sm:mx-0 sm:mb-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0 [&::-webkit-scrollbar]:hidden">
{facets.apps.map((app) => (
<Chip
key={app.domain}
active={filters.app === app.domain}
href={hrefFor({
...filters,
app: filters.app === app.domain ? undefined : app.domain,
})}
onNavigate={onNavigate}
title={`${num(app.count)} ${app.count === 1 ? 'file' : 'files'}`}
>
<AppTile
domain={app.domain}
size={16}
className="rounded-[5px]"
/>
{app.domain}
<span className="font-normal text-faint">{bytes(app.bytes)}</span>
</Chip>
))}
</div>
)}
</Card>
);
}