refactor: rclone system capability

This commit is contained in:
Nicolas Meienberger
2025-11-11 21:44:04 +01:00
parent 36b0282d18
commit 52e38a6242
9 changed files with 114 additions and 67 deletions

View File

@@ -22,6 +22,7 @@ import {
browseFilesystem,
listRepositories,
createRepository,
listRcloneRemotes,
deleteRepository,
getRepository,
listSnapshots,
@@ -29,7 +30,6 @@ import {
listSnapshotFiles,
restoreSnapshot,
doctorRepository,
listRcloneRemotes,
listBackupSchedules,
createBackupSchedule,
deleteBackupSchedule,
@@ -75,6 +75,7 @@ import type {
ListRepositoriesData,
CreateRepositoryData,
CreateRepositoryResponse,
ListRcloneRemotesData,
DeleteRepositoryData,
DeleteRepositoryResponse,
GetRepositoryData,
@@ -85,7 +86,6 @@ import type {
RestoreSnapshotResponse,
DoctorRepositoryData,
DoctorRepositoryResponse,
ListRcloneRemotesData,
ListBackupSchedulesData,
CreateBackupScheduleData,
CreateBackupScheduleResponse,
@@ -739,6 +739,27 @@ export const createRepositoryMutation = (
return mutationOptions;
};
export const listRcloneRemotesQueryKey = (options?: Options<ListRcloneRemotesData>) =>
createQueryKey("listRcloneRemotes", options);
/**
* List all configured rclone remotes on the host system
*/
export const listRcloneRemotesOptions = (options?: Options<ListRcloneRemotesData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await listRcloneRemotes({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: listRcloneRemotesQueryKey(options),
});
};
/**
* Delete a repository
*/
@@ -920,27 +941,6 @@ export const doctorRepositoryMutation = (
return mutationOptions;
};
export const listRcloneRemotesQueryKey = (options?: Options<ListRcloneRemotesData>) =>
createQueryKey("listRcloneRemotes", options);
/**
* List all configured rclone remotes on the host system
*/
export const listRcloneRemotesOptions = (options?: Options<ListRcloneRemotesData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await listRcloneRemotes({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: listRcloneRemotesQueryKey(options),
});
};
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) =>
createQueryKey("listBackupSchedules", options);

View File

@@ -46,6 +46,8 @@ import type {
ListRepositoriesResponses,
CreateRepositoryData,
CreateRepositoryResponses,
ListRcloneRemotesData,
ListRcloneRemotesResponses,
DeleteRepositoryData,
DeleteRepositoryResponses,
GetRepositoryData,
@@ -60,8 +62,6 @@ import type {
RestoreSnapshotResponses,
DoctorRepositoryData,
DoctorRepositoryResponses,
ListRcloneRemotesData,
ListRcloneRemotesResponses,
ListBackupSchedulesData,
ListBackupSchedulesResponses,
CreateBackupScheduleData,
@@ -357,6 +357,18 @@ export const createRepository = <ThrowOnError extends boolean = false>(
});
};
/**
* List all configured rclone remotes on the host system
*/
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(
options?: Options<ListRcloneRemotesData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/rclone-remotes",
...options,
});
};
/**
* Delete a repository
*/
@@ -445,18 +457,6 @@ export const doctorRepository = <ThrowOnError extends boolean = false>(
});
};
/**
* List all configured rclone remotes on the host system
*/
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(
options?: Options<ListRcloneRemotesData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/rclone-remotes",
...options,
});
};
/**
* List all backup schedules
*/

View File

@@ -828,6 +828,25 @@ export type CreateRepositoryResponses = {
export type CreateRepositoryResponse = CreateRepositoryResponses[keyof CreateRepositoryResponses];
export type ListRcloneRemotesData = {
body?: never;
path?: never;
query?: never;
url: "/api/v1/repositories/rclone-remotes";
};
export type ListRcloneRemotesResponses = {
/**
* List of rclone remotes
*/
200: Array<{
name: string;
type: string;
}>;
};
export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses];
export type DeleteRepositoryData = {
body?: never;
path: {
@@ -1052,25 +1071,6 @@ export type DoctorRepositoryResponses = {
export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof DoctorRepositoryResponses];
export type ListRcloneRemotesData = {
body?: never;
path?: never;
query?: never;
url: "/api/v1/repositories/rclone-remotes";
};
export type ListRcloneRemotesResponses = {
/**
* List of rclone remotes
*/
200: Array<{
name: string;
type: string;
}>;
};
export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses];
export type ListBackupSchedulesData = {
body?: never;
path?: never;
@@ -1648,6 +1648,7 @@ export type GetSystemInfoResponses = {
200: {
capabilities: {
docker: boolean;
rclone: boolean;
};
};
};

View File

@@ -9,10 +9,11 @@ import { Button } from "./ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { listRcloneRemotesOptions } from "~/api-client/@tanstack/react-query.gen";
import { getSystemInfoOptions, listRcloneRemotesOptions } from "~/api-client/@tanstack/react-query.gen";
import { useQuery } from "@tanstack/react-query";
import { Alert, AlertDescription } from "./ui/alert";
import { ExternalLink } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export const formSchema = type({
name: "2<=string<=32",
@@ -71,6 +72,10 @@ export const CreateRepositoryForm = ({
}
}, [watchedBackend, watchedName, form]);
const { data: systemInfo } = useQuery({
...getSystemInfoOptions(),
});
return (
<Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
@@ -113,7 +118,16 @@ export const CreateRepositoryForm = ({
<SelectItem value="s3">S3</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
<SelectItem value="azure">Azure Blob Storage</SelectItem>
<SelectItem value="rclone">rclone (40+ cloud providers)</SelectItem>
<Tooltip>
<TooltipTrigger>
<SelectItem disabled={!systemInfo?.capabilities.rclone} value="rclone">
rclone (40+ cloud providers)
</SelectItem>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: systemInfo?.capabilities.rclone })}>
<p>Setup rclone to use this backend</p>
</TooltipContent>
</Tooltip>
</SelectContent>
</Select>
<FormDescription>Choose the storage backend for this repository.</FormDescription>

View File

@@ -29,6 +29,7 @@ import { getVolume } from "~/api-client";
import { VolumeInfoTabContent } from "../tabs/info";
import { FilesTabContent } from "../tabs/files";
import { DockerTabContent } from "../tabs/docker";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
export function meta({ params }: Route.MetaArgs) {
return [
@@ -150,7 +151,16 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
<TabsList className="mb-2">
<TabsTrigger value="info">Configuration</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
{dockerAvailable && <TabsTrigger value="docker">Docker</TabsTrigger>}
<Tooltip>
<TooltipTrigger>
<TabsTrigger disabled={!dockerAvailable} value="docker">
Docker
</TabsTrigger>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: dockerAvailable })}>
<p>Enable Docker support to access this tab.</p>
</TooltipContent>
</Tooltip>
</TabsList>
<TabsContent value="info">
<VolumeInfoTabContent volume={volume} statfs={statfs} />