Skip to content

Commit e9d9fb3

Browse files
committed
Migrate reports
1 parent c57fbd8 commit e9d9fb3

File tree

3 files changed

+409
-4
lines changed

3 files changed

+409
-4
lines changed

app/.server/db/report.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { desc, eq, sql } from "drizzle-orm";
22

33
import { db } from "db/config.server";
4-
import { gataReport, oldReportFiles } from "db/schema";
4+
import { gataReport, oldReportFiles, cloudinaryImage, reportFiles } from "db/schema";
55
import { ReportType } from "~/types/GataReport.type";
66
import type { ReportSchema } from "~/utils/formSchema";
77

@@ -111,3 +111,35 @@ export const updateReportContent = async (reportId: string, content: string, log
111111
})
112112
.where(eq(gataReport.id, reportId));
113113
};
114+
115+
export const getAllReportsWithOldFiles = async () => {
116+
const reports = await db
117+
.select({
118+
id: gataReport.id,
119+
content: gataReport.content,
120+
})
121+
.from(gataReport);
122+
123+
const reportsWithFiles = await Promise.all(
124+
reports.map(async (report) => {
125+
const files = await db.select().from(oldReportFiles).where(eq(oldReportFiles.reportId, report.id));
126+
return { ...report, oldFiles: files };
127+
})
128+
);
129+
130+
return reportsWithFiles.filter((report) => report.oldFiles.length > 0);
131+
};
132+
133+
export const batchInsertCloudImages = async (images: Array<typeof cloudinaryImage.$inferInsert>) => {
134+
if (images.length === 0) return;
135+
await db.insert(cloudinaryImage).values(images);
136+
};
137+
138+
export const batchInsertReportFiles = async (reportFileRecords: Array<{ reportId: string; fileId: string }>) => {
139+
if (reportFileRecords.length === 0) return;
140+
await db.insert(reportFiles).values(reportFileRecords);
141+
};
142+
143+
export const updateReportContentOnly = async (reportId: string, content: string) => {
144+
await db.update(gataReport).set({ content }).where(eq(gataReport.id, reportId));
145+
};

app/routes/report.tsx

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { Plus } from "lucide-react";
2-
import { Link, Outlet } from "react-router";
1+
import { Plus, Upload } from "lucide-react";
2+
import { Form, Link, Outlet, useNavigation } from "react-router";
33

44
import { getReportsSimple } from "~/.server/db/report";
55
import { PageLayout } from "~/components/PageLayout";
66
import { ButtonResponsive } from "~/components/ui/button";
77
import { Typography } from "~/components/ui/typography";
88
import { ReportType } from "~/types/GataReport.type";
99
import { getRequiredUser } from "~/utils/auth.server";
10+
import { migrateAllReportFilesToBlob } from "~/utils/migrateReportFiles.server";
1011
import { isAdmin } from "~/utils/roleUtils";
1112

1213
import type { Route } from "./+types/report";
@@ -21,7 +22,36 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
2122
return { reports, loggedInUser };
2223
};
2324

24-
export default function ReportPage({ loaderData: { loggedInUser, reports } }: Route.ComponentProps) {
25+
export const action = async ({ request }: Route.ActionArgs) => {
26+
await getRequiredUser(request);
27+
28+
const formData = await request.formData();
29+
const intent = formData.get("intent");
30+
31+
if (intent === "migrateAllFiles") {
32+
try {
33+
const progress = await migrateAllReportFilesToBlob();
34+
return {
35+
success: true,
36+
message: `Migration complete: ${progress.successful} reports migrated successfully, ${progress.failed} failed. Total files: ${progress.totalFiles}`,
37+
progress,
38+
};
39+
} catch (error) {
40+
console.error("Migration error:", error);
41+
return {
42+
success: false,
43+
message: error instanceof Error ? error.message : "Migration failed with unknown error",
44+
};
45+
}
46+
}
47+
48+
return { success: false, message: "Unknown intent" };
49+
};
50+
51+
export default function ReportPage({ loaderData: { loggedInUser, reports }, actionData }: Route.ComponentProps) {
52+
const navigation = useNavigation();
53+
const isMigrating = navigation.state === "submitting" && navigation.formData?.get("intent") === "migrateAllFiles";
54+
2555
return (
2656
<PageLayout>
2757
<div className="flex justify-between items-center">
@@ -30,6 +60,42 @@ export default function ReportPage({ loaderData: { loggedInUser, reports } }: Ro
3060
</Typography>
3161
{isAdmin(loggedInUser) && <ButtonResponsive as={Link} to="new" label="Opprett" icon={<Plus />} />}
3262
</div>
63+
64+
{/* Migration Section */}
65+
<div className="my-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
66+
<Typography variant="h3" className="mb-2">
67+
Admin: File Migration
68+
</Typography>
69+
<Typography className="text-sm text-gray-600 mb-3">
70+
Migrate all report files from Cloudinary and oldReportFiles to Azure Blob Storage. This will update all
71+
content references to point directly to Azure URLs.
72+
</Typography>
73+
<Form method="post">
74+
<input type="hidden" name="intent" value="migrateAllFiles" />
75+
<ButtonResponsive
76+
as="button"
77+
type="submit"
78+
label={isMigrating ? "Migrating..." : "Migrate All Files to Azure"}
79+
icon={<Upload />}
80+
disabled={isMigrating}
81+
/>
82+
</Form>
83+
{actionData && (
84+
<div
85+
className={`mt-3 p-3 rounded ${
86+
actionData.success ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
87+
}`}
88+
>
89+
<Typography className="font-semibold">{actionData.message}</Typography>
90+
{actionData.progress && (
91+
<Typography className="text-sm mt-1">
92+
Reports processed: {actionData.progress.completed} / {actionData.progress.totalReports}
93+
</Typography>
94+
)}
95+
</div>
96+
)}
97+
</div>
98+
3399
<ul aria-labelledby="report-page-title" className="divide-y my-4">
34100
{reports.map((report) => {
35101
return (

0 commit comments

Comments
 (0)