page.tsx105 lines · main
1import Link from 'next/link';
2
3import { ArrowLeftRightIcon } from '@/components/ui/arrow-left-right';
4import { DatabaseIcon } from '@/components/ui/database';
5
6import { apiJson } from '@/lib/api';
7
8import { EmptyState } from '../_components/empty-state';
9import { Section } from '../_components/section';
10import { MigrationRequestRow } from './migration-request-row';
11
12export const metadata = { title: 'admin · migrations' };
13export const dynamic = 'force-dynamic';
14
15interface AdminRequest {
16 id: string;
17 userId: string | null;
18 orgId: string | null;
19 source: string;
20 sourceUrl: string | null;
21 sourceNotes: string;
22 estimatedTables: number | null;
23 estimatedRows: string | null;
24 estimatedFunctions: number | null;
25 urgency: string;
26 status: string;
27 contactEmail: string;
28 assignedTo: string | null;
29 operatorNotes: string;
30 createdAt: string;
31 updatedAt: string;
32}
33
34function publicApiOrigin(): string {
35 return process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? '';
36}
37
38export default async function AdminMigrationsPage() {
39 const { requests } = await apiJson<{ requests: AdminRequest[] }>(
40 '/v1/admin/migration-requests?limit=200',
41 ).catch(() => ({ requests: [] as AdminRequest[] }));
42
43 const open = requests.filter(
44 (r) => r.status !== 'completed' && r.status !== 'cancelled',
45 );
46 const closed = requests.filter(
47 (r) => r.status === 'completed' || r.status === 'cancelled',
48 );
49 const apiOrigin = publicApiOrigin();
50
51 return (
52 <div className="flex flex-col gap-10">
53 <header className="flex flex-col gap-2">
54 <div className="flex items-center gap-2">
55 <span className="text-[var(--color-primary)]">
56 <ArrowLeftRightIcon size={20} />
57 </span>
58 <h1 className="font-mono text-xl tracking-tight">migration requests</h1>
59 </div>
60 <p className="max-w-prose font-mono text-sm text-[var(--color-text-muted)]">
61 customer-submitted import requests from /dashboard/projects/new/migrate. triage
62 newest-first; bump status as you contact / schedule / complete each one.
63 status + operator notes mutations require fresh step-up auth.
64 </p>
65 </header>
66
67 <Section
68 title={`open · ${open.length}`}
69 icon={<ArrowLeftRightIcon size={16} />}
70 right={
71 <Link
72 href="/admin/migrations/funnel"
73 className="font-mono text-[10px] uppercase tracking-wider text-[var(--color-text-subtle)] transition hover:text-[var(--color-text-link)]"
74 >
75 funnel →
76 </Link>
77 }
78 >
79 {open.length === 0 ? (
80 <EmptyState
81 icon={<ArrowLeftRightIcon size={28} />}
82 title="no open requests"
83 message="the queue is clear."
84 />
85 ) : (
86 <ul className="flex flex-col gap-6">
87 {open.map((r) => (
88 <MigrationRequestRow key={r.id} request={r} apiOrigin={apiOrigin} />
89 ))}
90 </ul>
91 )}
92 </Section>
93
94 {closed.length > 0 ? (
95 <Section title={`closed · ${closed.length}`} icon={<DatabaseIcon size={16} />}>
96 <ul className="flex flex-col gap-6">
97 {closed.map((r) => (
98 <MigrationRequestRow key={r.id} request={r} apiOrigin={apiOrigin} />
99 ))}
100 </ul>
101 </Section>
102 ) : null}
103 </div>
104 );
105}