zerobyte/app/client/modules/repositories/routes/edit-repository.tsx
Nico d74f516336
feat: file-based provisionning of volumes and repos (#656)
* feat: file-based provisionning of volumes and repos

docs: provisioning example

chore: ui improvements

* chore: ci issues
2026-03-12 18:31:42 +01:00

183 lines
5.5 KiB
TypeScript

import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { Database } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { getRepositoryOptions, updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
import {
CreateRepositoryForm,
formSchema,
type RepositoryFormValues,
} from "~/client/modules/repositories/components/create-repository-form";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
import { parseError } from "~/client/lib/errors";
import { Alert, AlertDescription } from "~/client/components/ui/alert";
import type { RepositoryConfig } from "@zerobyte/core/restic";
import { useNavigate } from "@tanstack/react-router";
import { ManagedBadge } from "~/client/components/managed-badge";
const riskyLocationFieldsByBackend = {
local: ["path"],
s3: ["endpoint", "bucket"],
r2: ["endpoint", "bucket"],
gcs: ["bucket", "projectId"],
azure: ["container", "accountName", "endpointSuffix"],
rest: ["url", "path"],
sftp: ["host", "port", "path", "user"],
rclone: ["remote", "path"],
} as const;
const hasRiskyLocationChange = (initialConfig: RepositoryConfig, nextConfig: RepositoryConfig): boolean => {
const fields = riskyLocationFieldsByBackend[initialConfig.backend] ?? [];
return fields.some(
(field) => initialConfig[field as keyof RepositoryConfig] !== nextConfig[field as keyof RepositoryConfig],
);
};
export function EditRepositoryPage({ repositoryId }: { repositoryId: string }) {
const navigate = useNavigate();
const [showRiskConfirm, setShowRiskConfirm] = useState(false);
const [pendingValues, setPendingValues] = useState<RepositoryFormValues | null>(null);
const { data: repository } = useSuspenseQuery({
...getRepositoryOptions({ path: { shortId: repositoryId } }),
});
const updateRepository = useMutation({
...updateRepositoryMutation(),
onSuccess: async (data) => {
toast.success("Repository updated successfully");
setShowRiskConfirm(false);
setPendingValues(null);
void navigate({ to: `/repositories/${data.shortId}` });
},
onError: (error) => {
toast.error("Failed to update repository", {
description: parseError(error)?.message,
});
},
});
const initialConfig = repository.config as RepositoryConfig;
const initialValues: RepositoryFormValues = {
...initialConfig,
name: repository.name,
compressionMode: repository.compressionMode ?? "auto",
};
const submitUpdate = (values: RepositoryFormValues) => {
const { name, compressionMode, ...config } = formSchema.parse(values);
updateRepository.mutate({
path: { shortId: repositoryId },
body: {
name,
compressionMode,
config,
},
});
};
const handleSubmit = (values: RepositoryFormValues) => {
const { name: _name, compressionMode: _compressionMode, ...nextConfig } = formSchema.parse(values);
if (hasRiskyLocationChange(initialConfig, nextConfig)) {
setPendingValues(values);
setShowRiskConfirm(true);
return;
}
submitUpdate(values);
};
const handleSaveAnyway = () => {
if (!pendingValues) {
return;
}
submitUpdate(pendingValues);
};
const handleRiskConfirmOpenChange = (open: boolean) => {
setShowRiskConfirm(open);
if (!open) {
setPendingValues(null);
}
};
return (
<>
<div className="container mx-auto space-y-6">
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<Database className="w-5 h-5 text-primary" />
</div>
<div className="flex items-center gap-2">
<CardTitle>Edit Repository</CardTitle>
{repository.provisioningId && <ManagedBadge />}
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{updateRepository.isError && (
<Alert variant="destructive">
<AlertDescription>
<strong>Failed to update repository:</strong>
<br />
{parseError(updateRepository.error)?.message}
</AlertDescription>
</Alert>
)}
<CreateRepositoryForm
mode="update"
initialValues={initialValues}
onSubmit={handleSubmit}
loading={updateRepository.isPending}
/>
<div className="flex justify-end pt-2">
<Button
type="button"
variant="secondary"
onClick={() => navigate({ to: `/repositories/${repository.shortId}` })}
>
Cancel
</Button>
</div>
</CardContent>
</Card>
</div>
<AlertDialog open={showRiskConfirm} onOpenChange={handleRiskConfirmOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Repository location changed</AlertDialogTitle>
<AlertDialogDescription>
Changing endpoint, bucket, host, or path fields may point to a different repository location. Before
saving, ensure the repository already exists at the new target.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleSaveAnyway} disabled={updateRepository.isPending}>
Save anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}