Files
fluxer/packages/admin/src/pages/ArchivesPage.tsx
2026-02-17 12:22:36 +00:00

194 lines
5.7 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {Archive} from '@fluxer/admin/src/api/Archives';
import {ErrorAlert} from '@fluxer/admin/src/components/ErrorDisplay';
import {Layout} from '@fluxer/admin/src/components/Layout';
import {Badge} from '@fluxer/admin/src/components/ui/Badge';
import {PageHeader} from '@fluxer/admin/src/components/ui/Layout/PageHeader';
import {PageLayout} from '@fluxer/admin/src/components/ui/Layout/PageLayout';
import {VStack} from '@fluxer/admin/src/components/ui/Layout/VStack';
import {Text} from '@fluxer/admin/src/components/ui/Typography';
import type {Session} from '@fluxer/admin/src/types/App';
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
import {formatTimestamp} from '@fluxer/date_utils/src/DateFormatting';
import type {Flash} from '@fluxer/hono/src/Flash';
import type {UserAdminResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
import {Button} from '@fluxer/ui/src/components/Button';
import {EmptyState} from '@fluxer/ui/src/components/EmptyState';
import {Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow} from '@fluxer/ui/src/components/Table';
import type {FC} from 'hono/jsx';
export interface ArchivesPageProps {
config: Config;
session: Session;
currentAdmin: UserAdminResponse | undefined;
flash: Flash | undefined;
csrfToken: string;
subjectType: string;
subjectId: string | undefined;
archives: Array<Archive>;
error: string | undefined;
assetVersion: string;
}
function formatTimestampLocal(timestamp: string): string {
try {
return formatTimestamp(timestamp, 'en-US');
} catch {
return timestamp;
}
}
function getStatusLabel(archive: Archive): string {
if (archive.failed_at) {
return 'Failed';
}
if (archive.completed_at) {
return 'Completed';
}
return 'In Progress';
}
const ArchiveTable: FC<{archives: Array<Archive>; config: Config}> = ({archives, config}) => {
return (
<div class="overflow-hidden rounded-lg border border-neutral-200 bg-white">
<Table>
<TableHead>
<tr>
<TableHeaderCell label="Subject" />
<TableHeaderCell label="Requested By" />
<TableHeaderCell label="Requested At" />
<TableHeaderCell label="Status" />
<TableHeaderCell label="Actions" />
</tr>
</TableHead>
<TableBody>
{archives.map((archive) => (
<TableRow>
<TableCell>
<VStack gap={0} class="whitespace-nowrap">
<Text weight="semibold" size="sm">
{archive.subject_type} {archive.subject_id}
</Text>
<Text size="xs" color="muted">
Archive ID: {archive.archive_id}
</Text>
</VStack>
</TableCell>
<TableCell>
<Text size="sm">{archive.requested_by}</Text>
</TableCell>
<TableCell>
<Text size="sm">{formatTimestampLocal(archive.requested_at)}</Text>
</TableCell>
<TableCell>
<VStack gap={1}>
<div class="flex items-center gap-2">
<Badge size="sm" variant="neutral">
{getStatusLabel(archive)}
</Badge>
<Text size="xs" color="muted">
{archive.progress_percent}%
</Text>
</div>
{archive.progress_step && !archive.completed_at && !archive.failed_at && (
<Text size="xs" color="muted">
{archive.progress_step}
</Text>
)}
</VStack>
</TableCell>
<TableCell>
{archive.completed_at ? (
<Button
href={`${config.basePath}/archives/download?subject_type=${archive.subject_type}&subject_id=${archive.subject_id}&archive_id=${archive.archive_id}`}
variant="primary"
size="small"
>
Download
</Button>
) : (
<Text size="sm" color="muted">
Not ready
</Text>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
const ArchivesEmptyState: FC<{filterHint: string}> = ({filterHint}) => {
return (
<EmptyState
title={`No archives found${filterHint}.`}
message="This page lists all user and guild archives you've requested. Request an archive from a user or guild detail page."
/>
);
};
export async function ArchivesPage({
config,
session,
currentAdmin,
flash,
csrfToken,
subjectType,
subjectId,
archives,
error,
assetVersion,
}: ArchivesPageProps) {
const filterHint = subjectId ? ` for ${subjectType} ${subjectId}` : '';
return (
<Layout
csrfToken={csrfToken}
title="Archives"
activePage="archives"
config={config}
session={session}
currentAdmin={currentAdmin}
flash={flash}
assetVersion={assetVersion}
>
<PageLayout maxWidth="7xl">
<VStack gap={4}>
<PageHeader title={`Archives${filterHint}`} />
{error ? (
<ErrorAlert error={error} />
) : archives.length === 0 ? (
<ArchivesEmptyState filterHint={filterHint} />
) : (
<ArchiveTable archives={archives} config={config} />
)}
</VStack>
</PageLayout>
</Layout>
);
}