diff --git a/app/client/modules/backups/components/sortable-backup-card.tsx b/app/client/modules/backups/components/sortable-backup-card.tsx
new file mode 100644
index 00000000..8a7b98f5
--- /dev/null
+++ b/app/client/modules/backups/components/sortable-backup-card.tsx
@@ -0,0 +1,82 @@
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import { CalendarClock, Database, GripVertical, HardDrive } from "lucide-react";
+import { Link } from "react-router";
+import { BackupStatusDot } from "./backup-status-dot";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
+import type { ListBackupSchedulesResponse } from "~/client/api-client";
+
+type Schedule = ListBackupSchedulesResponse[number];
+
+interface SortableBackupCardProps {
+ schedule: Schedule;
+ isDragging?: boolean;
+}
+
+export function SortableBackupCard({ schedule, isDragging }: SortableBackupCardProps) {
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
+ id: schedule.id,
+ });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ };
+
+ return (
+
@@ -61,64 +135,30 @@ export default function Backups({ loaderData }: Route.ComponentProps) {
);
}
+ // Create a map for quick lookup
+ const scheduleMap = new Map(schedules.map((s) => [s.id, s]));
+
return (
-
- {schedules.map((schedule) => (
-
-
-
-
-
-
- {schedule.name}
-
-
-
-
-
- {schedule.volume.name}
- →
-
- {schedule.repository.name}
-
-
-
-
-
- Schedule
- {schedule.cronExpression}
-
-
- Last backup
-
- {schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleDateString() : "Never"}
-
-
-
- Next backup
-
- {schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleDateString() : "N/A"}
-
-
-
-
-
-
- ))}
-
-
-
-
- Create a backup job
-
-
-
-
+
+
+
+ {items.map((id) => {
+ const schedule = scheduleMap.get(id);
+ if (!schedule) return null;
+ return
;
+ })}
+
+
+
+
+ Create a backup job
+
+
+
+
+
+
);
}
diff --git a/app/drizzle/0022_add_backup_sort_order.sql b/app/drizzle/0022_add_backup_sort_order.sql
new file mode 100644
index 00000000..487a1bff
--- /dev/null
+++ b/app/drizzle/0022_add_backup_sort_order.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `backup_schedules_table` ADD `sort_order` integer DEFAULT 0 NOT NULL;
+
diff --git a/app/drizzle/meta/0022_snapshot.json b/app/drizzle/meta/0022_snapshot.json
new file mode 100644
index 00000000..bf0ce995
--- /dev/null
+++ b/app/drizzle/meta/0022_snapshot.json
@@ -0,0 +1,17 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "0022_add_backup_sort_order",
+ "prevId": "0021_steady_viper",
+ "tables": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
+
diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json
index 004e9b77..f4f849f8 100644
--- a/app/drizzle/meta/_journal.json
+++ b/app/drizzle/meta/_journal.json
@@ -155,6 +155,13 @@
"when": 1765307881092,
"tag": "0021_steady_viper",
"breakpoints": true
+ },
+ {
+ "idx": 22,
+ "version": "6",
+ "when": 1734278400000,
+ "tag": "0022_add_backup_sort_order",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts
index 55c741c2..cb94d29b 100644
--- a/app/server/db/schema.ts
+++ b/app/server/db/schema.ts
@@ -92,6 +92,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
lastBackupError: text("last_backup_error"),
nextBackupAt: int("next_backup_at", { mode: "number" }),
+ sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
});
diff --git a/app/server/modules/backups/backups.controller.ts b/app/server/modules/backups/backups.controller.ts
index 0bcc93eb..653cd659 100644
--- a/app/server/modules/backups/backups.controller.ts
+++ b/app/server/modules/backups/backups.controller.ts
@@ -16,6 +16,8 @@ import {
updateScheduleMirrorsDto,
updateScheduleMirrorsBody,
getMirrorCompatibilityDto,
+ reorderBackupSchedulesDto,
+ reorderBackupSchedulesBody,
type CreateBackupScheduleDto,
type DeleteBackupScheduleDto,
type GetBackupScheduleDto,
@@ -28,6 +30,7 @@ import {
type GetScheduleMirrorsDto,
type UpdateScheduleMirrorsDto,
type GetMirrorCompatibilityDto,
+ type ReorderBackupSchedulesDto,
} from "./backups.dto";
import { backupsService } from "./backups.service";
import {
@@ -139,4 +142,11 @@ export const backupScheduleController = new Hono()
const compatibility = await backupsService.getMirrorCompatibility(scheduleId);
return c.json
(compatibility, 200);
+ })
+ .post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => {
+ const body = c.req.valid("json");
+
+ await backupsService.reorderSchedules(body.scheduleIds);
+
+ return c.json({ success: true }, 200);
});
diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts
index fc771bf1..6173bd82 100644
--- a/app/server/modules/backups/backups.dto.ts
+++ b/app/server/modules/backups/backups.dto.ts
@@ -367,3 +367,34 @@ export const getMirrorCompatibilityDto = describeRoute({
},
},
});
+
+/**
+ * Reorder backup schedules
+ */
+export const reorderBackupSchedulesBody = type({
+ scheduleIds: "number[]",
+});
+
+export type ReorderBackupSchedulesBody = typeof reorderBackupSchedulesBody.infer;
+
+export const reorderBackupSchedulesResponse = type({
+ success: "boolean",
+});
+
+export type ReorderBackupSchedulesDto = typeof reorderBackupSchedulesResponse.infer;
+
+export const reorderBackupSchedulesDto = describeRoute({
+ description: "Reorder backup schedules by providing an array of schedule IDs in the desired order",
+ operationId: "reorderBackupSchedules",
+ tags: ["Backups"],
+ responses: {
+ 200: {
+ description: "Backup schedules reordered successfully",
+ content: {
+ "application/json": {
+ schema: resolver(reorderBackupSchedulesResponse),
+ },
+ },
+ },
+ },
+});
diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts
index 9994e503..372ac0d8 100644
--- a/app/server/modules/backups/backups.service.ts
+++ b/app/server/modules/backups/backups.service.ts
@@ -1,4 +1,4 @@
-import { and, eq, ne } from "drizzle-orm";
+import { and, asc, eq, ne } from "drizzle-orm";
import cron from "node-cron";
import { CronExpressionParser } from "cron-parser";
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
@@ -38,6 +38,7 @@ const listSchedules = async () => {
volume: true,
repository: true,
},
+ orderBy: [asc(backupSchedulesTable.sortOrder), asc(backupSchedulesTable.id)],
});
return schedules;
};
@@ -637,6 +638,16 @@ const getMirrorCompatibility = async (scheduleId: number) => {
return compatibility;
};
+const reorderSchedules = async (scheduleIds: number[]) => {
+ // Update each schedule's sortOrder based on its position in the array
+ for (let i = 0; i < scheduleIds.length; i++) {
+ await db
+ .update(backupSchedulesTable)
+ .set({ sortOrder: i, updatedAt: Date.now() })
+ .where(eq(backupSchedulesTable.id, scheduleIds[i]));
+ }
+};
+
export const backupsService = {
listSchedules,
getSchedule,
@@ -651,4 +662,5 @@ export const backupsService = {
getMirrors,
updateMirrors,
getMirrorCompatibility,
+ reorderSchedules,
};
diff --git a/package.json b/package.json
index e6d2fcb9..697d0fe4 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,9 @@
"studio": "drizzle-kit studio"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@hono/standard-validator": "^0.2.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",