* feat: file-based provisionning of volumes and repos docs: provisioning example chore: ui improvements * chore: ci issues
183 lines
5.5 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|