Skip to content
Open
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
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ ARG BUN_VERSION="1.3.1"

FROM oven/bun:${BUN_VERSION}-alpine AS base

RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client

RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client mariadb-client mysql-client postgresql-client

# ------------------------------
# DEPENDENCIES
Expand Down
262 changes: 261 additions & 1 deletion app/client/components/create-volume-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
import { volumeConfigSchema } from "~/schemas/volumes";
import { testConnectionMutation } from "../api-client/@tanstack/react-query.gen";

const SUPPORTS_CONNECTION_TEST = ["nfs", "smb", "webdav", "mariadb", "mysql", "postgres"] as const;

export const formSchema = type({
name: "2<=string<=32",
}).and(volumeConfigSchema);
Expand All @@ -35,6 +37,9 @@ const defaultValuesForType = {
nfs: { backend: "nfs" as const, port: 2049, version: "4.1" as const },
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
webdav: { backend: "webdav" as const, port: 80, ssl: false },
mariadb: { backend: "mariadb" as const, port: 3306 },
mysql: { backend: "mysql" as const, port: 3306 },
postgres: { backend: "postgres" as const, port: 5432, dumpFormat: "custom" as const },
};

export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
Expand Down Expand Up @@ -81,7 +86,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
const handleTestConnection = async () => {
const formValues = getValues();

if (formValues.backend === "nfs" || formValues.backend === "smb" || formValues.backend === "webdav") {
if (SUPPORTS_CONNECTION_TEST.includes(formValues.backend)) {
testBackendConnection.mutate({
body: { config: formValues },
});
Expand Down Expand Up @@ -130,6 +135,9 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
<SelectItem value="webdav">WebDAV</SelectItem>
<SelectItem value="mariadb">MariaDB</SelectItem>
<SelectItem value="mysql">MySQL</SelectItem>
<SelectItem value="postgres">PostgreSQL</SelectItem>
</SelectContent>
</Select>
<FormDescription>Choose the storage backend for this volume.</FormDescription>
Expand Down Expand Up @@ -546,6 +554,258 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
</>
)}

{watchedBackend === "mariadb" && (
<>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<FormControl>
<Input placeholder="localhost" {...field} />
</FormControl>
<FormDescription>MariaDB server hostname or IP address.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
defaultValue={3306}
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input type="number" placeholder="3306" {...field} />
</FormControl>
<FormDescription>MariaDB server port (default: 3306).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
<FormDescription>Database user with backup privileges.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for database authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="database"
render={({ field }) => (
<FormItem>
<FormLabel>Database</FormLabel>
<FormControl>
<Input placeholder="myapp_production" {...field} />
</FormControl>
<FormDescription>Name of the database to backup.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}

{watchedBackend === "mysql" && (
<>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<FormControl>
<Input placeholder="localhost" {...field} />
</FormControl>
<FormDescription>MySQL server hostname or IP address.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
defaultValue={3306}
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input type="number" placeholder="3306" {...field} />
</FormControl>
<FormDescription>MySQL server port (default: 3306).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
<FormDescription>Database user with backup privileges.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for database authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="database"
render={({ field }) => (
<FormItem>
<FormLabel>Database</FormLabel>
<FormControl>
<Input placeholder="myapp_production" {...field} />
</FormControl>
<FormDescription>Name of the database to backup.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}

{watchedBackend === "postgres" && (
<>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<FormControl>
<Input placeholder="localhost" {...field} />
</FormControl>
<FormDescription>PostgreSQL server hostname or IP address.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
defaultValue={5432}
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input type="number" placeholder="5432" {...field} />
</FormControl>
<FormDescription>PostgreSQL server port (default: 5432).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="postgres" {...field} />
</FormControl>
<FormDescription>Database user with backup privileges.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for database authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="database"
render={({ field }) => (
<FormItem>
<FormLabel>Database</FormLabel>
<FormControl>
<Input placeholder="myapp_production" {...field} />
</FormControl>
<FormDescription>Name of the database to backup.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dumpFormat"
defaultValue="custom"
render={({ field }) => (
<FormItem>
<FormLabel>Dump Format</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value || "custom"}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select dump format" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="custom">Custom (Compressed)</SelectItem>
<SelectItem value="plain">Plain SQL</SelectItem>
<SelectItem value="directory">Directory</SelectItem>
</SelectContent>
</Select>
<FormDescription>Format for database dumps (custom recommended).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}

{watchedBackend && watchedBackend !== "directory" && (
<div className="space-y-3">
<div className="flex items-center gap-2">
Expand Down
26 changes: 25 additions & 1 deletion app/client/components/volume-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Cloud, Folder, Server, Share2 } from "lucide-react";
import { Cloud, Database, Folder, Server, Share2 } from "lucide-react";
import type { BackendType } from "~/schemas/volumes";

type VolumeIconProps = {
Expand Down Expand Up @@ -32,6 +32,30 @@ const getIconAndColor = (backend: BackendType) => {
color: "text-green-600 dark:text-green-400",
label: "WebDAV",
};
case "mariadb":
return {
icon: Database,
color: "text-teal-600 dark:text-teal-400",
label: "MariaDB",
};
case "mysql":
return {
icon: Database,
color: "text-cyan-600 dark:text-cyan-400",
label: "MySQL",
};
case "postgres":
return {
icon: Database,
color: "text-indigo-600 dark:text-indigo-400",
label: "PostgreSQL",
};
case "sqlite":
return {
icon: Database,
color: "text-slate-600 dark:text-slate-400",
label: "SQLite",
};
default:
return {
icon: Folder,
Expand Down
11 changes: 7 additions & 4 deletions app/client/modules/volumes/routes/volume-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {

const { volume, statfs } = data;
const dockerAvailable = capabilities.docker;
const isDatabaseVolume = ["mariadb", "mysql", "postgres", "sqlite"].includes(volume.config.backend);

return (
<>
Expand Down Expand Up @@ -152,7 +153,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
<Tabs value={activeTab} onValueChange={(value) => setSearchParams({ tab: value })} className="mt-4">
<TabsList className="mb-2">
<TabsTrigger value="info">Configuration</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
{!isDatabaseVolume && <TabsTrigger value="files">Files</TabsTrigger>}
<Tooltip>
<TooltipTrigger>
<TabsTrigger disabled={!dockerAvailable} value="docker">
Expand All @@ -167,9 +168,11 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
<TabsContent value="info">
<VolumeInfoTabContent volume={volume} statfs={statfs} />
</TabsContent>
<TabsContent value="files">
<FilesTabContent volume={volume} />
</TabsContent>
{!isDatabaseVolume && (
<TabsContent value="files">
<FilesTabContent volume={volume} />
</TabsContent>
)}
{dockerAvailable && (
<TabsContent value="docker">
<DockerTabContent volume={volume} />
Expand Down
5 changes: 5 additions & 0 deletions app/client/modules/volumes/routes/volumes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
<SelectItem value="directory">Directory</SelectItem>
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
<SelectItem value="webdav">WebDAV</SelectItem>
<SelectItem value="mariadb">MariaDB</SelectItem>
<SelectItem value="mysql">MySQL</SelectItem>
<SelectItem value="postgres">PostgreSQL</SelectItem>
<SelectItem value="sqlite">SQLite</SelectItem>
</SelectContent>
</Select>
{(searchQuery || statusFilter || backendFilter) && (
Expand Down
Loading