refactor: pr feedbacks

This commit is contained in:
Nicolas Meienberger 2026-02-11 19:23:06 +01:00
parent caeb867554
commit 9422815843
10 changed files with 55 additions and 66 deletions

View file

@ -1,6 +1,6 @@
import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { Copy, Plus, Trash2 } from "lucide-react"; import { Copy, Plus, Trash2 } from "lucide-react";
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "~/client/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
@ -38,9 +38,9 @@ type MirrorAssignment = {
lastCopyError: string | null; lastCopyError: string | null;
}; };
export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => { const buildAssignments = (mirrors: GetScheduleMirrorsResponse) => {
const map = new Map<string, MirrorAssignment>(); const map = new Map<string, MirrorAssignment>();
for (const mirror of initialData) { for (const mirror of mirrors) {
map.set(mirror.repositoryId, { map.set(mirror.repositoryId, {
repositoryId: mirror.repositoryId, repositoryId: mirror.repositoryId,
enabled: mirror.enabled, enabled: mirror.enabled,
@ -49,8 +49,11 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
lastCopyError: mirror.lastCopyError, lastCopyError: mirror.lastCopyError,
}); });
} }
return map;
};
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(map); export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => {
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(() => buildAssignments(initialData));
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false);
@ -58,6 +61,12 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }), ...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
}); });
useEffect(() => {
if (!hasChanges) {
setAssignments(buildAssignments(currentMirrors));
}
}, [currentMirrors, hasChanges]);
const { data: compatibility } = useQuery({ const { data: compatibility } = useQuery({
...getMirrorCompatibilityOptions({ path: { scheduleId: scheduleId.toString() } }), ...getMirrorCompatibilityOptions({ path: { scheduleId: scheduleId.toString() } }),
}); });

View file

@ -30,7 +30,7 @@ export const clientLoader = async () => {
}; };
export function BackupsPage() { export function BackupsPage() {
const { data: schedules, isLoading } = useSuspenseQuery({ const { data: schedules } = useSuspenseQuery({
...listBackupSchedulesOptions(), ...listBackupSchedulesOptions(),
}); });
@ -69,14 +69,6 @@ export function BackupsPage() {
} }
}; };
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Loading backup schedules...</p>
</div>
);
}
if (!schedules || schedules.length === 0) { if (!schedules || schedules.length === 0) {
return ( return (
<EmptyState <EmptyState

View file

@ -21,7 +21,7 @@ export function CreateBackupPage() {
const formId = useId(); const formId = useId();
const [selectedVolumeId, setSelectedVolumeId] = useState<number | undefined>(); const [selectedVolumeId, setSelectedVolumeId] = useState<number | undefined>();
const { data: volumesData, isLoading: loadingVolumes } = useSuspenseQuery({ const { data: volumesData } = useSuspenseQuery({
...listVolumesOptions(), ...listVolumesOptions(),
}); });
@ -79,14 +79,6 @@ export function CreateBackupPage() {
const selectedVolume = volumesData.find((v) => v.id === selectedVolumeId); const selectedVolume = volumesData.find((v) => v.id === selectedVolumeId);
if (loadingVolumes) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Loading...</p>
</div>
);
}
if (!volumesData.length) { if (!volumesData.length) {
return ( return (
<EmptyState <EmptyState

View file

@ -122,32 +122,34 @@ export function SnapshotDetailsPage({ repositoryId, snapshotId }: { repositoryId
<span className="text-muted-foreground">Time:</span> <span className="text-muted-foreground">Time:</span>
<p>{formatDateTime(data.time)}</p> <p>{formatDateTime(data.time)}</p>
</div> </div>
<> {backupSchedule && (
<div> <>
<span className="text-muted-foreground">Backup Schedule:</span> <div>
<p> <span className="text-muted-foreground">Backup Schedule:</span>
<Link <p>
to="/backups/$scheduleId" <Link
className="text-primary hover:underline" to="/backups/$scheduleId"
params={{ scheduleId: backupSchedule?.shortId || "" }} className="text-primary hover:underline"
> params={{ scheduleId: backupSchedule.shortId }}
{backupSchedule?.name} >
</Link> {backupSchedule?.name}
</p> </Link>
</div> </p>
<div> </div>
<span className="text-muted-foreground">Volume:</span> <div>
<p> <span className="text-muted-foreground">Volume:</span>
<Link <p>
to={`/volumes/$volumeId`} <Link
className="text-primary hover:underline" to={`/volumes/$volumeId`}
params={{ volumeId: backupSchedule?.volume.shortId || "" }} className="text-primary hover:underline"
> params={{ volumeId: backupSchedule.volume.shortId }}
{backupSchedule?.volume.name} >
</Link> {backupSchedule?.volume.name}
</p> </Link>
</div> </p>
</> </div>
</>
)}
<div className="col-span-2"> <div className="col-span-2">
<span className="text-muted-foreground">Paths:</span> <span className="text-muted-foreground">Paths:</span>

View file

@ -11,7 +11,7 @@ export const authMiddleware = createMiddleware().server(async ({ next, request }
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname); const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
if (!session?.user?.id && !isAuthRoute) { if (!session?.user?.id && !isAuthRoute) {
const status = await getStatus(); const status = await getStatus().catch(() => ({ data: { hasUsers: false } }));
if (!status.data?.hasUsers) { if (!status.data?.hasUsers) {
throw redirect({ to: "/onboarding" }); throw redirect({ to: "/onboarding" });
} }

View file

@ -2,15 +2,16 @@ import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start"; import { createServerFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server"; import { getRequestHeaders } from "@tanstack/react-start/server";
import { Layout } from "~/client/components/layout"; import { Layout } from "~/client/components/layout";
import type { AppContext } from "~/context";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import { auth } from "~/server/lib/auth"; import { auth } from "~/server/lib/auth";
import { authService } from "~/server/modules/auth/auth.service";
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => { export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
const headers = getRequestHeaders(); const headers = getRequestHeaders();
const session = await auth.api.getSession({ headers }); const session = await auth.api.getSession({ headers });
const hasUsers = await authService.hasUsers();
return { user: session?.user, hasUsers: true }; return { user: session?.user ?? null, hasUsers };
}); });
export const Route = createFileRoute("/(dashboard)")({ export const Route = createFileRoute("/(dashboard)")({
@ -21,7 +22,7 @@ export const Route = createFileRoute("/(dashboard)")({
}, },
loader: async () => { loader: async () => {
const authContext = await fetchUser(); const authContext = await fetchUser();
return authContext as AppContext; return authContext;
}, },
}); });

View file

@ -48,6 +48,7 @@ const envSchema = type({
appVersion: s.APP_VERSION, appVersion: s.APP_VERSION,
trustedOrigins: s.TRUSTED_ORIGINS?.split(",") trustedOrigins: s.TRUSTED_ORIGINS?.split(",")
.map((origin) => origin.trim()) .map((origin) => origin.trim())
.filter(Boolean)
.concat(s.BASE_URL) ?? [s.BASE_URL], .concat(s.BASE_URL) ?? [s.BASE_URL],
disableRateLimiting: s.DISABLE_RATE_LIMITING === "true", disableRateLimiting: s.DISABLE_RATE_LIMITING === "true",
appSecret: s.APP_SECRET, appSecret: s.APP_SECRET,

View file

@ -99,7 +99,6 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-bundle-analyzer": "^1.2.3", "vite-bundle-analyzer": "^1.2.3",
"vite-plugin-babel": "^1.4.1",
"vite-tsconfig-paths": "^6.0.5", "vite-tsconfig-paths": "^6.0.5",
"wait-for-expect": "^4.0.0", "wait-for-expect": "^4.0.0",
}, },
@ -1349,7 +1348,7 @@
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"nf3": ["nf3@0.3.9", "", {}, "sha512-vgmoL6pUXpKXx+WepG4H1xc2nf1N/5vKgU9QerryuSFIyef8EDkj7esYcIgqMe/mE6nGsy6M6b8wfssEBQizVQ=="], "nf3": ["nf3@0.3.10", "", {}, "sha512-UlqmHkZiHGgSkRj17yrOXEsSu5ECvtlJ3Xm1W5WsWrTKgu9m7OjrMZh9H/ME2LcWrTlMD0/vmmNVpyBG4yRdGg=="],
"nitro": ["nitro@3.0.1-alpha.2", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.5", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-YviDY5J/trS821qQ1fpJtpXWIdPYiOizC/meHavlm1Hfuhx//H+Egd1+4C5SegJRgtWMnRPW9n//6Woaw81cTQ=="], "nitro": ["nitro@3.0.1-alpha.2", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.5", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-YviDY5J/trS821qQ1fpJtpXWIdPYiOizC/meHavlm1Hfuhx//H+Egd1+4C5SegJRgtWMnRPW9n//6Woaw81cTQ=="],
@ -1621,8 +1620,6 @@
"vite-bundle-analyzer": ["vite-bundle-analyzer@1.3.6", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-elFXkF9oGy4CJEeOdP1DSEHRDKWfmaEDfT9/GuF4jmyfmUF6nC//iN+ythqMEVvrtchOEHGKLumZwnu1NjoI0w=="], "vite-bundle-analyzer": ["vite-bundle-analyzer@1.3.6", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-elFXkF9oGy4CJEeOdP1DSEHRDKWfmaEDfT9/GuF4jmyfmUF6nC//iN+ythqMEVvrtchOEHGKLumZwnu1NjoI0w=="],
"vite-plugin-babel": ["vite-plugin-babel@1.4.1", "", { "peerDependencies": { "@babel/core": "^7.0.0", "vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-quO+viHGSv1cjbfhbeiMZ7SZpo8P29NiUh9LJfKhpmIDwy0THRiTRUbanBbkNcZcSyHFgp1n7TByd1C2kanqLQ=="],
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="], "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],

View file

@ -120,7 +120,6 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-bundle-analyzer": "^1.2.3", "vite-bundle-analyzer": "^1.2.3",
"vite-plugin-babel": "^1.4.1",
"vite-tsconfig-paths": "^6.0.5", "vite-tsconfig-paths": "^6.0.5",
"wait-for-expect": "^4.0.0" "wait-for-expect": "^4.0.0"
}, },

View file

@ -4,18 +4,10 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { nitro } from "nitro/vite"; import { nitro } from "nitro/vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import viteReact from "@vitejs/plugin-react"; import viteReact from "@vitejs/plugin-react";
import babel from "vite-plugin-babel";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tsconfigPaths(), tsconfigPaths(),
babel({
filter: /\.[jt]sx?$/,
babelConfig: {
presets: ["@babel/preset-typescript"],
plugins: [["babel-plugin-react-compiler"]],
},
}),
tanstackStart({ tanstackStart({
srcDirectory: "app", srcDirectory: "app",
router: { router: {
@ -23,7 +15,11 @@ export default defineConfig({
}, },
}), }),
nitro({ preset: "bun" }), nitro({ preset: "bun" }),
viteReact(), viteReact({
babel: {
plugins: ["babel-plugin-react-compiler"],
},
}),
tailwindcss(), tailwindcss(),
], ],
build: { build: {