chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQueryClient } from '@tanstack/react-query';
import {
CircleAlertIcon,
CircleCheckIcon,
LoaderCircleIcon,
} from 'lucide-react';
import { useState } from 'react';
import { Badge } from '#/components/atoms/badge.jsx';
import { Button } from '#/components/atoms/button.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { RowIcon } from '#/components/atoms/row-icon.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { ListRow } from '#/components/molecules/list-row.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { post } from '#/lib/api.js';
import { bytes, num, timeAgo } from '#/lib/format.js';
import { queryKeys } from '#/lib/query.js';
const STATUS = {
ok: { icon: CircleCheckIcon, label: 'Backed up' },
error: { icon: CircleAlertIcon, label: 'Backup failed' },
running: { icon: LoaderCircleIcon, label: 'Backing up' },
};
/**
* What each backup did, newest first, with the "Back up now" trigger. A manual
* run blocks the button until the server answers with the finished run (or an
* error), then the whole backups query refreshes so the list shows it.
* @param {{data: {runs: object[]}}} props
*/
export function BackupRunsCard({ data }) {
const qc = useQueryClient();
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const runs = data.runs || [];
const runNow = async () => {
setError('');
setNotice('');
setBusy(true);
try {
const run = await post('/backups/run');
setNotice(
`Backup finished — ${bytes(run.bytesUploaded) || '0 B'} uploaded to ${run.snapshot}.`,
);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setBusy(false);
qc.invalidateQueries({ queryKey: queryKeys.backups });
}
};
return (
<Panel>
<Section
title="Backup history"
description="What each backup did, newest first."
action={
<Button variant="outline" size="sm" disabled={busy} onClick={runNow}>
{busy && <LoaderCircleIcon className="animate-spin" />}
{busy ? 'Backing up…' : 'Back up now'}
</Button>
}
>
<ErrorNotice error={error} className="mb-4" />
{notice && !error && (
<Notice variant="success" className="mb-4">
{notice}
</Notice>
)}
<div className="flex flex-col gap-1">
{runs.map((run) => {
const status = STATUS[run.status] || STATUS.running;
const Icon = status.icon;
const facts = [
// "Started" fits a run still going; a finished run says when it
// wrapped up so it does not read as stuck.
run.status === 'running'
? run.startedAt && `Started ${timeAgo(run.startedAt)}`
: run.finishedAt && `Finished ${timeAgo(run.finishedAt)}`,
// Mid-run the total is still being discovered; show only what
// has actually been copied so far. Finished runs upload only
// blobs the target does not already hold, so say "new" rather
// than implying a progress fraction.
run.status === 'running'
? `${num(run.blobsCopied)} blobs copied`
: `${num(run.blobsCopied)} new blobs, ${num(run.blobsTotal)} total`,
run.bytesUploaded != null &&
`${bytes(run.bytesUploaded)} uploaded`,
].filter(Boolean);
return (
<ListRow
key={run.id}
variant="plain"
media={
<RowIcon>
<Icon
className={run.status === 'running' ? 'animate-spin' : ''}
/>
</RowIcon>
}
title={
<>
{status.label}
<Badge>{run.trigger}</Badge>
{run.keyIncluded && (
<Badge variant="scope">
{run.keyStale ? 'stale key' : 'key included'}
</Badge>
)}
{run.blobsFailed > 0 && (
<Badge>
{num(run.blobsFailed)}{' '}
{run.blobsFailed === 1 ? 'blob' : 'blobs'} not copied
</Badge>
)}
</>
}
description={
<>
<span className="block">{facts.join(' · ')}</span>
{run.error && (
<span className="block text-destructive">
{run.error}
</span>
)}
{/* A run that copied the repo but not every blob. The
next run tries those blobs again. */}
{!run.error && run.blobError && (
<span className="block text-warning">
{run.blobError}
</span>
)}
</>
}
/>
);
})}
{runs.length === 0 && (
<p className="px-1 py-2 text-[13px] text-muted-foreground">
No backups have run yet.
</p>
)}
</div>
</Section>
</Panel>
);
}