mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Enhance backup functionality by introducing support for compose backups. Update backup schema to include serviceName and backupType fields. Modify related components to handle new backup types and integrate service selection for compose backups. Update API routes and database schema accordingly.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -31,42 +32,59 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { DatabaseZap, PlusIcon, RefreshCw } from "lucide-react";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
const AddPostgresBackup1Schema = z.object({
|
const AddPostgresBackup1Schema = z.object({
|
||||||
destinationId: z.string().min(1, "Destination required"),
|
destinationId: z.string().min(1, "Destination required"),
|
||||||
schedule: z.string().min(1, "Schedule (Cron) required"),
|
schedule: z.string().min(1, "Schedule (Cron) required"),
|
||||||
// .regex(
|
|
||||||
// new RegExp(
|
|
||||||
// /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/,
|
|
||||||
// ),
|
|
||||||
// "Invalid Cron",
|
|
||||||
// ),
|
|
||||||
prefix: z.string().min(1, "Prefix required"),
|
prefix: z.string().min(1, "Prefix required"),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
database: z.string().min(1, "Database required"),
|
database: z.string().min(1, "Database required"),
|
||||||
keepLatestCount: z.coerce.number().optional(),
|
keepLatestCount: z.coerce.number().optional(),
|
||||||
|
serviceName: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string;
|
id: string;
|
||||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
|
backupType: "database" | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
export const AddBackup = ({
|
||||||
|
id,
|
||||||
|
databaseType,
|
||||||
|
refetch,
|
||||||
|
backupType = "database",
|
||||||
|
}: Props) => {
|
||||||
const { data, isLoading } = api.destination.all.useQuery();
|
const { data, isLoading } = api.destination.all.useQuery();
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
|
||||||
const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } =
|
const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } =
|
||||||
api.backup.create.useMutation();
|
api.backup.create.useMutation();
|
||||||
@@ -79,10 +97,28 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
prefix: "/",
|
prefix: "/",
|
||||||
schedule: "",
|
schedule: "",
|
||||||
keepLatestCount: undefined,
|
keepLatestCount: undefined,
|
||||||
|
serviceName: null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddPostgresBackup1Schema),
|
resolver: zodResolver(AddPostgresBackup1Schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: backupType === "compose",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
database: databaseType === "web-server" ? "dokploy" : "",
|
database: databaseType === "web-server" ? "dokploy" : "",
|
||||||
@@ -91,32 +127,45 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
prefix: "/",
|
prefix: "/",
|
||||||
schedule: "",
|
schedule: "",
|
||||||
keepLatestCount: undefined,
|
keepLatestCount: undefined,
|
||||||
|
serviceName: null,
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddPostgresBackup) => {
|
const onSubmit = async (data: AddPostgresBackup) => {
|
||||||
|
if (backupType === "compose" && !data.serviceName) {
|
||||||
|
form.setError("serviceName", {
|
||||||
|
type: "manual",
|
||||||
|
message: "Service name is required for compose backups",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const getDatabaseId =
|
const getDatabaseId =
|
||||||
databaseType === "postgres"
|
backupType === "compose"
|
||||||
? {
|
? {
|
||||||
postgresId: databaseId,
|
composeId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "mariadb"
|
: databaseType === "postgres"
|
||||||
? {
|
? {
|
||||||
mariadbId: databaseId,
|
postgresId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "mysql"
|
: databaseType === "mariadb"
|
||||||
? {
|
? {
|
||||||
mysqlId: databaseId,
|
mariadbId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "mongo"
|
: databaseType === "mysql"
|
||||||
? {
|
? {
|
||||||
mongoId: databaseId,
|
mysqlId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "web-server"
|
: databaseType === "mongo"
|
||||||
? {
|
? {
|
||||||
userId: databaseId,
|
mongoId: id,
|
||||||
}
|
}
|
||||||
: undefined;
|
: databaseType === "web-server"
|
||||||
|
? {
|
||||||
|
userId: id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await createBackup({
|
await createBackup({
|
||||||
destinationId: data.destinationId,
|
destinationId: data.destinationId,
|
||||||
@@ -125,8 +174,10 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
database: data.database,
|
database: data.database,
|
||||||
keepLatestCount: data.keepLatestCount,
|
keepLatestCount: data.keepLatestCount,
|
||||||
databaseType,
|
databaseType: databaseType,
|
||||||
|
serviceName: data.serviceName,
|
||||||
...getDatabaseId,
|
...getDatabaseId,
|
||||||
|
backupType,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Backup Created");
|
toast.success("Backup Created");
|
||||||
@@ -157,6 +208,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock type="warning" className="[overflow-wrap:anywhere]">
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destinationId"
|
name="destinationId"
|
||||||
@@ -232,6 +288,110 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{backupType === "compose" && (
|
||||||
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value || undefined}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{(!services || services.length === 0) && (
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from the
|
||||||
|
last deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="database"
|
name="database"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import type { ServiceType } from "../../application/advanced/show-resources";
|
|||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string;
|
id: string;
|
||||||
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
}
|
}
|
||||||
@@ -85,11 +85,7 @@ const formatBytes = (bytes: number): string => {
|
|||||||
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RestoreBackup = ({
|
export const RestoreBackup = ({ id, databaseType, serverId }: Props) => {
|
||||||
databaseId,
|
|
||||||
databaseType,
|
|
||||||
serverId,
|
|
||||||
}: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
@@ -136,7 +132,7 @@ export const RestoreBackup = ({
|
|||||||
|
|
||||||
api.backup.restoreBackupWithLogs.useSubscription(
|
api.backup.restoreBackupWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
databaseId,
|
databaseId: id,
|
||||||
databaseType,
|
databaseType,
|
||||||
databaseName: form.watch("databaseName"),
|
databaseName: form.watch("databaseName"),
|
||||||
backupFile: form.watch("backupFile"),
|
backupFile: form.watch("backupFile"),
|
||||||
|
|||||||
@@ -22,41 +22,60 @@ import type { ServiceType } from "../../application/advanced/show-resources";
|
|||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
import { RestoreBackup } from "./restore-backup";
|
import { RestoreBackup } from "./restore-backup";
|
||||||
import { UpdateBackup } from "./update-backup";
|
import { UpdateBackup } from "./update-backup";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||||
|
backupType?: "database" | "compose";
|
||||||
}
|
}
|
||||||
export const ShowBackups = ({ id, type }: Props) => {
|
export const ShowBackups = ({
|
||||||
|
id,
|
||||||
|
databaseType,
|
||||||
|
backupType = "database",
|
||||||
|
}: Props) => {
|
||||||
const [activeManualBackup, setActiveManualBackup] = useState<
|
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const queryMap = {
|
const queryMap =
|
||||||
postgres: () =>
|
backupType === "database"
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
? {
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
postgres: () =>
|
||||||
mariadb: () =>
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
mysql: () =>
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
"web-server": () => api.user.getBackups.useQuery(),
|
mariadb: () =>
|
||||||
};
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
mongo: () =>
|
||||||
|
api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
"web-server": () => api.user.getBackups.useQuery(),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
compose: () =>
|
||||||
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
const { data } = api.destination.all.useQuery();
|
const { data } = api.destination.all.useQuery();
|
||||||
const { data: postgres, refetch } = queryMap[type]
|
const key = backupType === "database" ? databaseType : "compose";
|
||||||
? queryMap[type]()
|
const query = queryMap[key as keyof typeof queryMap];
|
||||||
|
const { data: postgres, refetch } = query
|
||||||
|
? query()
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
console.log(postgres);
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
postgres: () => api.backup.manualBackupPostgres.useMutation(),
|
postgres: () => api.backup.manualBackupPostgres.useMutation(),
|
||||||
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||||
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||||
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||||
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
||||||
|
compose: () => api.backup.manualBackupCompose.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||||
type
|
databaseType
|
||||||
]
|
]
|
||||||
? mutationMap[type]()
|
? mutationMap[databaseType]()
|
||||||
: api.backup.manualBackupMongo.useMutation();
|
: api.backup.manualBackupMongo.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: deleteBackup, isLoading: isRemoving } =
|
const { mutateAsync: deleteBackup, isLoading: isRemoving } =
|
||||||
@@ -78,16 +97,17 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
{type !== "web-server" && (
|
{databaseType !== "web-server" && (
|
||||||
<AddBackup
|
<AddBackup
|
||||||
databaseId={id}
|
id={id}
|
||||||
databaseType={type}
|
databaseType={databaseType}
|
||||||
|
backupType={backupType}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
databaseId={id}
|
id={id}
|
||||||
databaseType={type}
|
databaseType={databaseType}
|
||||||
serverId={"serverId" in postgres ? postgres.serverId : undefined}
|
serverId={"serverId" in postgres ? postgres.serverId : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,7 +130,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="flex flex-col gap-4 w-full">
|
||||||
{postgres?.backups.length === 0 ? (
|
{postgres?.backups.length === 0 ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
@@ -119,13 +139,14 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||||
<AddBackup
|
<AddBackup
|
||||||
databaseId={id}
|
id={id}
|
||||||
databaseType={type}
|
databaseType={databaseType}
|
||||||
|
backupType={backupType}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
databaseId={id}
|
id={id}
|
||||||
databaseType={type}
|
databaseType={databaseType}
|
||||||
serverId={
|
serverId={
|
||||||
"serverId" in postgres ? postgres.serverId : undefined
|
"serverId" in postgres ? postgres.serverId : undefined
|
||||||
}
|
}
|
||||||
@@ -133,12 +154,41 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
{backupType === "compose" && (
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Deploy is required to apply changes after creating or
|
||||||
|
updating a backup.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{postgres?.backups.map((backup) => (
|
{postgres?.backups.map((backup) => (
|
||||||
<div key={backup.backupId}>
|
<div key={backup.backupId}>
|
||||||
<div className="flex w-full flex-col md:flex-row md:items-center justify-between gap-4 md:gap-10 border rounded-lg p-4">
|
<div className="flex w-full flex-col md:flex-row md:items-center justify-between gap-4 md:gap-10 border rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-6 flex-col gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-8 flex-col gap-8">
|
||||||
|
{backup.backupType === "compose" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">
|
||||||
|
Service Name
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{backup.serviceName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">
|
||||||
|
Database Type
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{backup.databaseType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Destination</span>
|
<span className="font-medium">Destination</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -31,16 +32,37 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
DatabaseZap,
|
||||||
|
PenBoxIcon,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
const UpdateBackupSchema = z.object({
|
const UpdateBackupSchema = z.object({
|
||||||
destinationId: z.string().min(1, "Destination required"),
|
destinationId: z.string().min(1, "Destination required"),
|
||||||
schedule: z.string().min(1, "Schedule (Cron) required"),
|
schedule: z.string().min(1, "Schedule (Cron) required"),
|
||||||
@@ -48,6 +70,7 @@ const UpdateBackupSchema = z.object({
|
|||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
database: z.string().min(1, "Database required"),
|
database: z.string().min(1, "Database required"),
|
||||||
keepLatestCount: z.coerce.number().optional(),
|
keepLatestCount: z.coerce.number().optional(),
|
||||||
|
serviceName: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateBackup = z.infer<typeof UpdateBackupSchema>;
|
type UpdateBackup = z.infer<typeof UpdateBackupSchema>;
|
||||||
@@ -59,6 +82,7 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
const { data, isLoading } = api.destination.all.useQuery();
|
const { data, isLoading } = api.destination.all.useQuery();
|
||||||
const { data: backup } = api.backup.one.useQuery(
|
const { data: backup } = api.backup.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -69,6 +93,24 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: backup?.composeId || "",
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled:
|
||||||
|
isOpen && backup?.backupType === "compose" && !!backup?.composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { mutateAsync, isLoading: isLoadingUpdate } =
|
const { mutateAsync, isLoading: isLoadingUpdate } =
|
||||||
api.backup.update.useMutation();
|
api.backup.update.useMutation();
|
||||||
|
|
||||||
@@ -80,6 +122,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
prefix: "/",
|
prefix: "/",
|
||||||
schedule: "",
|
schedule: "",
|
||||||
keepLatestCount: undefined,
|
keepLatestCount: undefined,
|
||||||
|
serviceName: null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(UpdateBackupSchema),
|
resolver: zodResolver(UpdateBackupSchema),
|
||||||
});
|
});
|
||||||
@@ -92,6 +135,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
enabled: backup.enabled || false,
|
enabled: backup.enabled || false,
|
||||||
prefix: backup.prefix,
|
prefix: backup.prefix,
|
||||||
schedule: backup.schedule,
|
schedule: backup.schedule,
|
||||||
|
serviceName: backup.serviceName || null,
|
||||||
keepLatestCount: backup.keepLatestCount
|
keepLatestCount: backup.keepLatestCount
|
||||||
? Number(backup.keepLatestCount)
|
? Number(backup.keepLatestCount)
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -100,6 +144,14 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
}, [form, form.reset, backup]);
|
}, [form, form.reset, backup]);
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateBackup) => {
|
const onSubmit = async (data: UpdateBackup) => {
|
||||||
|
if (backup?.backupType === "compose" && !data.serviceName) {
|
||||||
|
form.setError("serviceName", {
|
||||||
|
type: "manual",
|
||||||
|
message: "Service name is required for compose backups",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
backupId,
|
backupId,
|
||||||
destinationId: data.destinationId,
|
destinationId: data.destinationId,
|
||||||
@@ -107,6 +159,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
schedule: data.schedule,
|
schedule: data.schedule,
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
database: data.database,
|
database: data.database,
|
||||||
|
serviceName: data.serviceName,
|
||||||
keepLatestCount: data.keepLatestCount as number | null,
|
keepLatestCount: data.keepLatestCount as number | null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -143,6 +196,11 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock type="warning" className="[overflow-wrap:anywhere]">
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destinationId"
|
name="destinationId"
|
||||||
@@ -218,6 +276,110 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{backup?.backupType === "compose" && (
|
||||||
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value || undefined}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{(!services || services.length === 0) && (
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from the
|
||||||
|
last deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="database"
|
name="database"
|
||||||
|
|||||||
5
apps/dokploy/drizzle/0088_same_ezekiel.sql
Normal file
5
apps/dokploy/drizzle/0088_same_ezekiel.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TYPE "public"."backupType" AS ENUM('database', 'compose');--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "backupType" "backupType" DEFAULT 'database' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "composeId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
|
||||||
5448
apps/dokploy/drizzle/meta/0088_snapshot.json
Normal file
5448
apps/dokploy/drizzle/meta/0088_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -617,6 +617,13 @@
|
|||||||
"when": 1745723563822,
|
"when": 1745723563822,
|
||||||
"tag": "0087_lively_risque",
|
"tag": "0087_lively_risque",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 88,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1745801614194,
|
||||||
|
"tag": "0088_same_ezekiel",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"
|
|||||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||||
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
||||||
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||||
|
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||||
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
||||||
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
@@ -30,6 +31,13 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -57,6 +65,8 @@ type TabState =
|
|||||||
| "domains"
|
| "domains"
|
||||||
| "monitoring";
|
| "monitoring";
|
||||||
|
|
||||||
|
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo";
|
||||||
|
|
||||||
const Service = (
|
const Service = (
|
||||||
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
||||||
) => {
|
) => {
|
||||||
@@ -65,6 +75,8 @@ const Service = (
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setTab] = useState<TabState>(activeTab);
|
const [tab, setTab] = useState<TabState>(activeTab);
|
||||||
|
const [selectedDatabaseType, setSelectedDatabaseType] =
|
||||||
|
useState<DatabaseType>("postgres");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.query.tab) {
|
if (router.query.tab) {
|
||||||
@@ -217,16 +229,17 @@ const Service = (
|
|||||||
className={cn(
|
className={cn(
|
||||||
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
isCloud && data?.serverId
|
isCloud && data?.serverId
|
||||||
? "lg:grid-cols-7"
|
? "lg:grid-cols-8"
|
||||||
: data?.serverId
|
: data?.serverId
|
||||||
? "lg:grid-cols-6"
|
? "lg:grid-cols-7"
|
||||||
: "lg:grid-cols-7",
|
: "lg:grid-cols-8",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
@@ -245,6 +258,34 @@ const Service = (
|
|||||||
<ShowEnvironment id={composeId} type="compose" />
|
<ShowEnvironment id={composeId} type="compose" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<Label>Database Type</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedDatabaseType}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setSelectedDatabaseType(value as DatabaseType)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Select a database type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="postgres">PostgreSQL</SelectItem>
|
||||||
|
<SelectItem value="mariadb">MariaDB</SelectItem>
|
||||||
|
<SelectItem value="mysql">MySQL</SelectItem>
|
||||||
|
<SelectItem value="mongo">MongoDB</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<ShowBackups
|
||||||
|
id={composeId}
|
||||||
|
databaseType={selectedDatabaseType}
|
||||||
|
backupType="compose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="pt-2.5">
|
<div className="pt-2.5">
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ const Mariadb = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups id={mariadbId} type="mariadb" />
|
<ShowBackups id={mariadbId} databaseType="mariadb" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -272,7 +272,11 @@ const Mongo = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups id={mongoId} type="mongo" />
|
<ShowBackups
|
||||||
|
id={mongoId}
|
||||||
|
databaseType="mongo"
|
||||||
|
backupType="database"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -252,7 +252,11 @@ const MySql = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups id={mysqlId} type="mysql" />
|
<ShowBackups
|
||||||
|
id={mysqlId}
|
||||||
|
databaseType="mysql"
|
||||||
|
backupType="database"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -251,7 +251,11 @@ const Postgresql = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups id={postgresId} type="postgres" />
|
<ShowBackups
|
||||||
|
id={postgresId}
|
||||||
|
databaseType="postgres"
|
||||||
|
backupType="database"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ const Page = () => {
|
|||||||
<WebServer />
|
<WebServer />
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
<ShowBackups id={user?.userId ?? ""} type="web-server" />
|
<ShowBackups
|
||||||
|
id={user?.userId ?? ""}
|
||||||
|
databaseType="web-server"
|
||||||
|
backupType="database"
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
serverId = backup.mongo.serverId;
|
serverId = backup.mongo.serverId;
|
||||||
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||||
serverId = backup.mariadb.serverId;
|
serverId = backup.mariadb.serverId;
|
||||||
|
} else if (
|
||||||
|
backup.backupType === "compose" &&
|
||||||
|
backup.compose?.serverId
|
||||||
|
) {
|
||||||
|
serverId = backup.compose.serverId;
|
||||||
}
|
}
|
||||||
const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
@@ -232,6 +237,13 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
manualBackupCompose: protectedProcedure
|
||||||
|
.input(apiFindOneBackup)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
// const backup = await findBackupById(input.backupId);
|
||||||
|
// await runComposeBackup(backup);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
manualBackupMongo: protectedProcedure
|
manualBackupMongo: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
|||||||
@@ -723,4 +723,18 @@ export const composeRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
manualBackup: protectedProcedure
|
||||||
|
.input(z.object({ composeId: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to backup this compose",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createBackup({
|
||||||
|
composeId: compose.composeId,
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { mongo } from "./mongo";
|
|||||||
import { mysql } from "./mysql";
|
import { mysql } from "./mysql";
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
import { users_temp } from "./user";
|
import { users_temp } from "./user";
|
||||||
|
import { compose } from "./compose";
|
||||||
|
|
||||||
export const databaseType = pgEnum("databaseType", [
|
export const databaseType = pgEnum("databaseType", [
|
||||||
"postgres",
|
"postgres",
|
||||||
"mariadb",
|
"mariadb",
|
||||||
@@ -24,6 +26,8 @@ export const databaseType = pgEnum("databaseType", [
|
|||||||
"web-server",
|
"web-server",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||||
|
|
||||||
export const backups = pgTable("backup", {
|
export const backups = pgTable("backup", {
|
||||||
backupId: text("backupId")
|
backupId: text("backupId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -33,14 +37,19 @@ export const backups = pgTable("backup", {
|
|||||||
enabled: boolean("enabled"),
|
enabled: boolean("enabled"),
|
||||||
database: text("database").notNull(),
|
database: text("database").notNull(),
|
||||||
prefix: text("prefix").notNull(),
|
prefix: text("prefix").notNull(),
|
||||||
|
serviceName: text("serviceName"),
|
||||||
destinationId: text("destinationId")
|
destinationId: text("destinationId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||||
|
|
||||||
keepLatestCount: integer("keepLatestCount"),
|
keepLatestCount: integer("keepLatestCount"),
|
||||||
|
backupType: backupType("backupType").notNull().default("database"),
|
||||||
databaseType: databaseType("databaseType").notNull(),
|
databaseType: databaseType("databaseType").notNull(),
|
||||||
|
composeId: text("composeId").references(
|
||||||
|
(): AnyPgColumn => compose.composeId,
|
||||||
|
{
|
||||||
|
onDelete: "cascade",
|
||||||
|
},
|
||||||
|
),
|
||||||
postgresId: text("postgresId").references(
|
postgresId: text("postgresId").references(
|
||||||
(): AnyPgColumn => postgres.postgresId,
|
(): AnyPgColumn => postgres.postgresId,
|
||||||
{
|
{
|
||||||
@@ -87,6 +96,10 @@ export const backupsRelations = relations(backups, ({ one }) => ({
|
|||||||
fields: [backups.userId],
|
fields: [backups.userId],
|
||||||
references: [users_temp.id],
|
references: [users_temp.id],
|
||||||
}),
|
}),
|
||||||
|
compose: one(compose, {
|
||||||
|
fields: [backups.composeId],
|
||||||
|
references: [compose.composeId],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(backups, {
|
const createSchema = createInsertSchema(backups, {
|
||||||
@@ -118,6 +131,9 @@ export const apiCreateBackup = createSchema.pick({
|
|||||||
mongoId: true,
|
mongoId: true,
|
||||||
databaseType: true,
|
databaseType: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
backupType: true,
|
||||||
|
composeId: true,
|
||||||
|
serviceName: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiFindOneBackup = createSchema
|
export const apiFindOneBackup = createSchema
|
||||||
@@ -141,5 +157,6 @@ export const apiUpdateBackup = createSchema
|
|||||||
destinationId: true,
|
destinationId: true,
|
||||||
database: true,
|
database: true,
|
||||||
keepLatestCount: true,
|
keepLatestCount: true,
|
||||||
|
serviceName: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { server } from "./server";
|
|||||||
import { applicationStatus, triggerType } from "./shared";
|
import { applicationStatus, triggerType } from "./shared";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { backups } from "./backups";
|
||||||
|
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
@@ -135,6 +136,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
|||||||
fields: [compose.serverId],
|
fields: [compose.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
|
backups: many(backups),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(compose, {
|
const createSchema = createInsertSchema(compose, {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const findBackupById = async (backupId: string) => {
|
|||||||
mariadb: true,
|
mariadb: true,
|
||||||
mongo: true,
|
mongo: true,
|
||||||
destination: true,
|
destination: true,
|
||||||
|
compose: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
|
|||||||
@@ -131,6 +131,11 @@ export const findComposeById = async (composeId: string) => {
|
|||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
gitea: true,
|
gitea: true,
|
||||||
server: true,
|
server: true,
|
||||||
|
backups: {
|
||||||
|
with: {
|
||||||
|
destination: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user