Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions app/client/components/ui/collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "~/client/lib/utils";

interface CollapsibleProps extends React.HTMLAttributes<HTMLDivElement> {
open?: boolean;
onOpenChange?: (open: boolean) => void;
defaultOpen?: boolean;
}

const CollapsibleContext = React.createContext<{
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}>({
open: false,
setOpen: () => {},
});

const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
({ className, open: controlledOpen, onOpenChange, defaultOpen = false, children, ...props }, ref) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);

const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;

const setOpen = React.useCallback(
(value: React.SetStateAction<boolean>) => {
const newValue = typeof value === "function" ? value(open) : value;
if (!isControlled) {
setUncontrolledOpen(newValue);
}
onOpenChange?.(newValue);
},
[isControlled, open, onOpenChange],
);

return (
<CollapsibleContext.Provider value={{ open, setOpen }}>
<div ref={ref} className={cn(className)} {...props}>
{children}
</div>
</CollapsibleContext.Provider>
);
},
);
Collapsible.displayName = "Collapsible";

interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}

const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
({ className, children, ...props }, ref) => {
const { open, setOpen } = React.useContext(CollapsibleContext);

return (
<button
ref={ref}
type="button"
className={cn(
"flex w-full items-center justify-between py-2 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
data-state={open ? "open" : "closed"}
onClick={() => setOpen(!open)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</button>
);
},
);
CollapsibleTrigger.displayName = "CollapsibleTrigger";

interface CollapsibleContentProps extends React.HTMLAttributes<HTMLDivElement> {}

const CollapsibleContent = React.forwardRef<HTMLDivElement, CollapsibleContentProps>(
({ className, children, ...props }, ref) => {
const { open } = React.useContext(CollapsibleContext);

return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
)}
data-state={open ? "open" : "closed"}
hidden={!open}
{...props}
>
{open && children}
</div>
);
},
);
CollapsibleContent.displayName = "CollapsibleContent";

export { Collapsible, CollapsibleTrigger, CollapsibleContent };
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
RcloneRepositoryForm,
RestRepositoryForm,
SftpRepositoryForm,
AdvancedForm,
} from "./repository-forms";

export const formSchema = type({
Expand Down Expand Up @@ -268,6 +269,8 @@ export const CreateRepositoryForm = ({
{watchedBackend === "rest" && <RestRepositoryForm form={form} />}
{watchedBackend === "sftp" && <SftpRepositoryForm form={form} />}

<AdvancedForm form={form} />

{mode === "update" && (
<Button type="submit" className="w-full" loading={loading}>
<Save className="h-4 w-4 mr-2" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../../../../components/ui/form";
import { Textarea } from "../../../../components/ui/textarea";
import { Checkbox } from "../../../../components/ui/checkbox";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible";
import type { RepositoryFormValues } from "../create-repository-form";
import { cn } from "~/client/lib/utils";

type Props = {
form: UseFormReturn<RepositoryFormValues>;
};

export const AdvancedForm = ({ form }: Props) => {
const insecureTls = form.watch("insecureTls");
const cacert = form.watch("cacert");

return (
<Collapsible>
<CollapsibleTrigger className="w-full text-muted-foreground hover:no-underline">
Advanced Settings
</CollapsibleTrigger>
<CollapsibleContent className="pb-4 space-y-4">
<FormField
control={form.control}
name="insecureTls"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Checkbox
checked={field.value ?? false}
disabled={!!cacert}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: !cacert })}>
<p className="max-w-xs">
This option is disabled because a CA certificate is provided. Remove the CA certificate to skip
TLS validation instead.
</p>
</TooltipContent>
</Tooltip>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Skip TLS certificate verification</FormLabel>
<FormDescription>
Disable TLS certificate verification for HTTPS connections with self-signed certificates. This is
insecure and should only be used for testing.
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cacert"
render={({ field }) => (
<FormItem>
<FormLabel>CA Certificate (Optional)</FormLabel>
<FormControl>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Textarea
placeholder={"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
rows={6}
disabled={insecureTls}
{...field}
/>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: !insecureTls })}>
<p className="max-w-xs">
CA certificate is disabled because TLS validation is being skipped. Uncheck "Skip TLS Certificate
Verification" to provide a custom CA certificate.
</p>
</TooltipContent>
</Tooltip>
</FormControl>
<FormDescription>
Custom CA certificate for self-signed certificates (PEM format). This applies to HTTPS
connections.&nbsp;
<a
href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#rest-server"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Learn more
</a>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { AzureRepositoryForm } from "./azure-repository-form";
export { RcloneRepositoryForm } from "./rclone-repository-form";
export { RestRepositoryForm } from "./rest-repository-form";
export { SftpRepositoryForm } from "./sftp-repository-form";
export { AdvancedForm } from "./advanced-tls-form";
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,13 @@ import {
} from "../../../../components/ui/form";
import { Input } from "../../../../components/ui/input";
import { SecretInput } from "../../../../components/ui/secret-input";
import { Textarea } from "../../../../components/ui/textarea";
import { Checkbox } from "../../../../components/ui/checkbox";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip";
import type { RepositoryFormValues } from "../create-repository-form";
import { cn } from "~/client/lib/utils";

type Props = {
form: UseFormReturn<RepositoryFormValues>;
};

export const RestRepositoryForm = ({ form }: Props) => {
const insecureTls = form.watch("insecureTls");
const cacert = form.watch("cacert");

return (
<>
<FormField
Expand All @@ -39,83 +32,6 @@ export const RestRepositoryForm = ({ form }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="insecureTls"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Checkbox
checked={field.value ?? false}
disabled={!!cacert}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: !cacert })}>
<p className="max-w-xs">
This option is disabled because a CA certificate is provided. Remove the CA certificate to skip TLS
validation instead.
</p>
</TooltipContent>
</Tooltip>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Skip TLS certificate verification</FormLabel>
<FormDescription>
Disable TLS certificate verification if rest server is https and uses a self-signed certificate. This is
insecure and should only be used for testing.
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cacert"
render={({ field }) => (
<FormItem>
<FormLabel>CA Certificate (Optional)</FormLabel>
<FormControl>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Textarea
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
rows={6}
disabled={insecureTls}
{...field}
/>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: !insecureTls })}>
<p className="max-w-xs">
CA certificate is disabled because TLS validation is being skipped. Uncheck "Skip TLS Certificate
Verification" to provide a custom CA certificate.
</p>
</TooltipContent>
</Tooltip>
</FormControl>
<FormDescription>
Custom CA certificate for self-signed certificates (PEM format).&nbsp;
<a
href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#rest-server"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Learn more
</a>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
Expand Down
10 changes: 6 additions & 4 deletions app/client/modules/repositories/tabs/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from "~/client/components/ui/alert-dialog";
import type { Repository } from "~/client/lib/types";
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
import type { CompressionMode } from "~/schemas/restic";
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";

type Props = {
repository: Repository;
Expand Down Expand Up @@ -59,6 +59,8 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
const hasChanges =
name !== repository.name || compressionMode !== ((repository.compressionMode as CompressionMode) || "off");

const config = repository.config as RepositoryConfig;

return (
<>
<Card className="p-6">
Expand Down Expand Up @@ -116,19 +118,19 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
{repository.lastChecked ? new Date(repository.lastChecked).toLocaleString() : "Never"}
</p>
</div>
{repository.config.backend === "rest" && repository.config.cacert && (
{config.cacert && (
<div>
<div className="text-sm font-medium text-muted-foreground">CA Certificate</div>
<p className="mt-1 text-sm">
<span className="text-green-500">configured</span>
</p>
</div>
)}
{repository.config.backend === "rest" && "insecureTls" in repository.config && (
{"insecureTls" in config && (
<div>
<div className="text-sm font-medium text-muted-foreground">TLS Certificate Validation</div>
<p className="mt-1 text-sm">
{repository.config.insecureTls ? (
{config.insecureTls ? (
<span className="text-red-500">disabled</span>
) : (
<span className="text-green-500">enabled</span>
Expand Down
4 changes: 2 additions & 2 deletions app/schemas/restic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
const baseRepositoryConfigSchema = type({
isExistingRepository: "boolean?",
customPassword: "string?",
cacert: "string?",
insecureTls: "boolean?",
});

export const s3RepositoryConfigSchema = type({
Expand Down Expand Up @@ -68,8 +70,6 @@ export const restRepositoryConfigSchema = type({
username: "string?",
password: "string?",
path: "string?",
cacert: "string?",
insecureTls: "boolean?",
}).and(baseRepositoryConfigSchema);

export const sftpRepositoryConfigSchema = type({
Expand Down
Loading