{ "name": "migration-list", "type": "registry:ui", "title": "Migration List", "description": "Applied and pending migrations, sorted by version. A migration applied after a later version is flagged — that is the signature of two branches merged without a rebase, and it means this schema matches no single branch.", "category": "Database", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "fmt", "lib-motion", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/migration-list.tsx", "type": "registry:ui", "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Check, Clock, TriangleAlert, X } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Fmt } from '@/components/ui/fmt'\nimport { enterFade } from '@/lib/motion'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Database migrations, applied and pending.\n *\n * Order is the version string, never the array order. Migrations are named by\n * timestamp precisely so they sort deterministically, and a list that trusts\n * the order it was handed will show a rebase artefact as the real sequence.\n *\n * A migration applied *out of order* — one whose version predates an\n * already-applied migration — is flagged. That is the signature of two branches\n * merged without rebasing, and it means the schema on this database is not the\n * schema any single branch describes.\n *\n * Irreversible migrations are marked. Knowing there is no down-path before you\n * run something is the difference between a rollback and a restore from backup.\n */\nexport type MigrationState = 'applied' | 'pending' | 'failed' | 'rolled-back'\n\nexport type Migration = {\n /** Sortable identifier — usually a timestamp prefix. */\n version: string\n name: ReactNode\n state: MigrationState\n appliedAt?: Date\n /** Execution time in milliseconds. */\n duration?: number\n /** No down migration exists. */\n irreversible?: boolean\n error?: ReactNode\n}\n\nconst STATE = {\n applied: { label: 'Applied', color: 'green', Icon: Check },\n pending: { label: 'Pending', color: 'amber', Icon: Clock },\n failed: { label: 'Failed', color: 'destructive', Icon: X },\n 'rolled-back': { label: 'Rolled back', color: 'neutral', Icon: TriangleAlert },\n} as const\n\n/**\n * Default label formatters, hoisted out of the parameter list.\n *\n * An arrow function written inline as a default parameter is a value the\n * React Compiler cannot safely reorder, so it bails on the whole component\n * and none of it gets auto-memoised. At module scope it is a stable\n * reference and the component compiles.\n */\nconst DEFAULT_PENDING_SUMMARY: (count: number) => ReactNode = (count) => `${count} pending`\n\nfunction MigrationList({\n migrations,\n now,\n locale = 'en-GB',\n emptyLabel = 'No migrations',\n irreversibleLabel = 'irreversible',\n outOfOrderLabel = 'out of order',\n outOfOrderNote = 'Applied after a later version — the usual sign of two branches merged without a rebase.',\n pendingSummary = DEFAULT_PENDING_SUMMARY,\n className,\n ...props\n}: ComponentProps<'div'> & {\n migrations: Migration[]\n now?: Date\n locale?: string\n emptyLabel?: ReactNode\n irreversibleLabel?: ReactNode\n outOfOrderLabel?: ReactNode\n outOfOrderNote?: ReactNode\n pendingSummary?: (count: number) => ReactNode\n}) {\n // Sorted by version, never by the order the caller happened to supply.\n const rows = [...migrations].sort((a, b) => a.version.localeCompare(b.version))\n\n // Walk in the order they were actually applied: a migration whose version\n // precedes one already run is a merge artefact, not a sequence.\n const ordered = [...rows].sort((a, b) => {\n const at = a.appliedAt?.getTime() ?? Infinity\n const bt = b.appliedAt?.getTime() ?? Infinity\n return at - bt\n })\n const outOfOrder = new Set()\n let seen = ''\n for (const row of ordered) {\n if (row.state !== 'applied') continue\n if (seen && row.version < seen) outOfOrder.add(row.version)\n else seen = row.version\n }\n\n const pending = rows.filter((row) => row.state === 'pending').length\n\n if (rows.length === 0) {\n return (\n
\n

{emptyLabel}

\n
\n )\n }\n\n return (\n \n {pending > 0 && (\n
\n \n {pendingSummary(pending)}\n
\n )}\n\n \n \n )\n}\n\nexport { MigrationList }\n" } ] }