From 1017f1a38bb354375e45c5689cd4c95c57d26f33 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:49:33 +0100 Subject: [PATCH] feat: edit repository form (#507) * feat: edit repository form * refactor: local repo path concat as a code migration * refactor: server constants * chore: fix lint issue in test file * refactor: add auth to getServerConstants --- .gitignore | 1 + app/client/api-client/types.gen.ts | 197 ++++++++++-- app/client/components/snapshots-table.tsx | 4 +- app/client/lib/constants.ts | 2 - .../backups/components/backup-card.tsx | 2 +- .../components/snapshot-file-browser.tsx | 4 +- .../components/create-repository-form.tsx | 21 +- .../local-repository-form.tsx | 18 +- .../repositories/routes/edit-repository.tsx | 176 ++++++++++ .../routes/repository-details.tsx | 2 +- .../repositories/routes/snapshot-details.tsx | 8 +- app/client/modules/repositories/tabs/info.tsx | 302 +++++++----------- app/routeTree.gen.ts | 183 ++++++----- .../$snapshotId.restore.tsx} | 4 +- .../{$scheduleId.tsx => $backupId/index.tsx} | 18 +- .../$snapshotId/index.tsx} | 20 +- .../$snapshotId/restore.tsx} | 22 +- .../repositories/$repositoryId/edit.tsx | 37 +++ .../index.tsx} | 8 +- app/schemas/restic.ts | 3 +- app/server/core/constants.ts | 2 + .../lib/auth-middlewares/only-one-user.ts | 2 +- app/server/lib/functions/server-constants.ts | 19 ++ app/server/modules/lifecycle/migrations.ts | 3 +- .../migrations/00004-concat-path-name.ts | 96 ++++++ .../__tests__/repositories.controller.test.ts | 132 ++++++++ .../modules/repositories/repositories.dto.ts | 4 + .../repositories/repositories.service.ts | 108 +++++-- app/server/modules/system/system.service.ts | 2 +- app/server/utils/restic.ts | 9 +- app/test/helpers/repository.ts | 2 +- 31 files changed, 1037 insertions(+), 374 deletions(-) delete mode 100644 app/client/lib/constants.ts create mode 100644 app/client/modules/repositories/routes/edit-repository.tsx rename app/routes/(dashboard)/backups/{$backupId.$snapshotId.restore.tsx => $backupId/$snapshotId.restore.tsx} (99%) rename app/routes/(dashboard)/backups/{$scheduleId.tsx => $backupId/index.tsx} (84%) rename app/routes/(dashboard)/repositories/{$repoId.$snapshotId.tsx => $repositoryId/$snapshotId/index.tsx} (73%) rename app/routes/(dashboard)/repositories/{$repoId.$snapId.restore.tsx => $repositoryId/$snapshotId/restore.tsx} (66%) create mode 100644 app/routes/(dashboard)/repositories/$repositoryId/edit.tsx rename app/routes/(dashboard)/repositories/{$repositoryId.tsx => $repositoryId/index.tsx} (97%) create mode 100644 app/server/lib/functions/server-constants.ts create mode 100644 app/server/modules/lifecycle/migrations/00004-concat-path-name.ts diff --git a/.gitignore b/.gitignore index 6d1d89b2..f5ebc411 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ playwright/temp # OpenAPI error logs openapi-ts-error-*.log .output +tmp/ diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 9ac44460..8e135a75 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -841,7 +841,7 @@ export type ListRepositoriesResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -851,7 +851,6 @@ export type ListRepositoriesResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -1034,7 +1033,7 @@ export type CreateRepositoryData = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -1044,7 +1043,6 @@ export type CreateRepositoryData = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -1279,7 +1277,7 @@ export type GetRepositoryResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -1289,7 +1287,6 @@ export type GetRepositoryResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -1387,6 +1384,172 @@ export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryRe export type UpdateRepositoryData = { body?: { compressionMode?: "auto" | "max" | "off"; + config?: + | { + accessKeyId: string; + backend: "r2"; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + } + | { + accessKeyId: string; + backend: "s3"; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + } + | { + accountKey: string; + accountName: string; + backend: "azure"; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + } + | { + backend: "gcs"; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + } + | { + backend: "local"; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + } + | { + backend: "rclone"; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + } + | { + backend: "rest"; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + username?: string; + } + | { + backend: "sftp"; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; + }; name?: string; }; path: { @@ -1397,6 +1560,10 @@ export type UpdateRepositoryData = { }; export type UpdateRepositoryErrors = { + /** + * Invalid repository update payload + */ + 400: unknown; /** * Repository not found */ @@ -1499,7 +1666,7 @@ export type UpdateRepositoryResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -1509,7 +1676,6 @@ export type UpdateRepositoryResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -2077,7 +2243,7 @@ export type ListBackupSchedulesResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -2087,7 +2253,6 @@ export type ListBackupSchedulesResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -2460,7 +2625,7 @@ export type GetBackupScheduleResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -2470,7 +2635,6 @@ export type GetBackupScheduleResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -2824,7 +2988,7 @@ export type GetBackupScheduleForVolumeResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -2834,7 +2998,6 @@ export type GetBackupScheduleForVolumeResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -3400,7 +3563,7 @@ export type GetScheduleMirrorsResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -3410,7 +3573,6 @@ export type GetScheduleMirrorsResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -3620,7 +3782,7 @@ export type UpdateScheduleMirrorsResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -3630,7 +3792,6 @@ export type UpdateScheduleMirrorsResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index f87d7110..bcd96847 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -172,8 +172,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
{backupSchedule?.name}
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index 41c1c66a..b19170c3 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -1,33 +1,27 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
-import { Check, Save, Square, Stethoscope, Trash2, Unlock } from "lucide-react";
+import { Pencil, Square, Stethoscope, Trash2, Unlock } from "lucide-react";
import { Card } from "~/client/components/ui/card";
import { Button } from "~/client/components/ui/button";
-import { Input } from "~/client/components/ui/input";
-import { Label } from "~/client/components/ui/label";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
- AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import type { Repository } from "~/client/lib/types";
-import { REPOSITORY_BASE } from "~/client/lib/constants";
import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime";
import {
cancelDoctorMutation,
deleteRepositoryMutation,
startDoctorMutation,
unlockRepositoryMutation,
- updateRepositoryMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
-import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
+import type { RepositoryConfig } from "~/schemas/restic";
import { DoctorReport } from "../components/doctor-report";
import { parseError } from "~/client/lib/errors";
import { useNavigate } from "@tanstack/react-router";
@@ -37,38 +31,16 @@ type Props = {
};
const getEffectiveLocalPath = (repository: Repository): string | null => {
- if (repository.type !== "local") return null;
- const config = repository.config as { name: string; path?: string; isExistingRepository?: boolean };
-
- if (config.isExistingRepository) {
- return config.path ?? null;
- }
-
- const basePath = config.path || REPOSITORY_BASE;
- return `${basePath}/${config.name}`;
+ if (repository.config.backend !== "local") return null;
+ return repository.config.path;
};
export const RepositoryInfoTabContent = ({ repository }: Props) => {
- const [name, setName] = useState(repository.name);
- const [compressionMode, setCompressionMode] = useState