mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(multi-server): add modals and separate server actions
This commit is contained in:
@@ -11,7 +11,7 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||
|
||||
@@ -122,7 +122,7 @@ export const columns: ColumnDef<Container>[] = [
|
||||
</ShowDockerModalLogs>
|
||||
<ShowContainerConfig
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId}
|
||||
serverId={container.serverId || ""}
|
||||
/>
|
||||
<DockerTerminalModal containerId={container.containerId}>
|
||||
Terminal
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||
|
||||
export const ShowDokployActions = () => {
|
||||
const { mutateAsync: reloadServer, isLoading } =
|
||||
api.settings.reloadServer.useMutation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||
<Button isLoading={isLoading} variant="outline">
|
||||
Server
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadServer()
|
||||
.then(async () => {
|
||||
toast.success("Server Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.success("Server Reloaded");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy">
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||
import { CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { ShowStorageActions } from "./show-storage-actions";
|
||||
import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ShowServerActions = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Actions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 w-full gap-4">
|
||||
<ShowTraefikActions serverId={serverId} />
|
||||
<ShowStorageActions serverId={serverId} />
|
||||
<ToggleDockerCleanup serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowStorageActions = ({ serverId }: Props) => {
|
||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||
api.settings.cleanAll.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanDockerBuilder,
|
||||
isLoading: cleanDockerBuilderIsLoading,
|
||||
} = api.settings.cleanDockerBuilder.useMutation();
|
||||
|
||||
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
|
||||
api.settings.cleanMonitoring.useMutation();
|
||||
const {
|
||||
mutateAsync: cleanUnusedImages,
|
||||
isLoading: cleanUnusedImagesIsLoading,
|
||||
} = api.settings.cleanUnusedImages.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanUnusedVolumes,
|
||||
isLoading: cleanUnusedVolumesIsLoading,
|
||||
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanStoppedContainers,
|
||||
isLoading: cleanStoppedContainersIsLoading,
|
||||
} = api.settings.cleanStoppedContainers.useMutation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isLoading={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
Space
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedImages({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned images");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean images");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused images</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedVolumes({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned volumes");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean volumes");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused volumes</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanStoppedContainers({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Stopped containers cleaned");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean stopped containers");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean stopped containers</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanDockerBuilder({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Docker Builder");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Docker Builder");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Docker Builder & System</span>
|
||||
</DropdownMenuItem>
|
||||
{!serverId && (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanMonitoring()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Monitoring");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Monitoring");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Monitoring </span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanAll({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned all");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean all");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean all</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,15 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -8,37 +19,27 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ShowModalLogs } from "../web-server/show-modal-logs";
|
||||
import { DockerTerminalModal } from "../web-server/docker-terminal-modal";
|
||||
import { EditTraefikEnv } from "../web-server/edit-traefik-env";
|
||||
import { ShowMainTraefikConfig } from "../web-server/show-main-traefik-config";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const TraefikActions = ({ serverId }: Props) => {
|
||||
api.settings.reloadServer.useMutation();
|
||||
export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||
api.settings.reloadTraefik.useMutation();
|
||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||
api.settings.cleanAll.useMutation();
|
||||
|
||||
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||
api.settings.toggleDashboard.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanStoppedContainers,
|
||||
isLoading: cleanStoppedContainersIsLoading,
|
||||
} = api.settings.cleanStoppedContainers.useMutation();
|
||||
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
|
||||
const { mutateAsync: updateDockerCleanup } =
|
||||
api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||
api.settings.haveTraefikDashboardPortEnabled.useQuery();
|
||||
api.settings.haveTraefikDashboardPortEnabled.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -72,20 +73,9 @@ export const TraefikActions = ({ serverId }: Props) => {
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy-traefik">
|
||||
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
{!serverId && (
|
||||
<ShowMainTraefikConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View Traefik config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowMainTraefikConfig>
|
||||
)}
|
||||
|
||||
<EditTraefikEnv serverId={serverId}>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
@@ -119,15 +109,15 @@ export const TraefikActions = ({ serverId }: Props) => {
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DockerTerminalModal appName="dokploy-traefik" serverId={serverId}>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<span>Enter the terminal</span>
|
||||
</DropdownMenuItem>
|
||||
</DockerTerminalModal>
|
||||
{/*
|
||||
<DockerTerminalModal appName="dokploy-traefik">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<span>Enter the terminal</span>
|
||||
</DropdownMenuItem>
|
||||
</DockerTerminalModal> */}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
|
||||
{
|
||||
serverId: serverId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
|
||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={async (e) => {
|
||||
await mutateAsync({
|
||||
enableDockerCleanup: e,
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Docker Cleanup Enabled");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Docker Cleanup Error");
|
||||
});
|
||||
|
||||
if (serverId) {
|
||||
refetchServer();
|
||||
} else {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { ContainerIcon, FileTextIcon } from "lucide-react";
|
||||
import { ContainerIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
|
||||
import { ShowContainers } from "../../docker/show/show-containers";
|
||||
|
||||
interface Props {
|
||||
@@ -42,7 +41,6 @@ export const ShowDockerContainersModal = ({ serverId }: Props) => {
|
||||
|
||||
<div className="grid w-full gap-1">
|
||||
<ShowContainers serverId={serverId} />
|
||||
{/* <ShowTraefikSystem serverId={serverId} /> */}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TraefikActions } from "./traefik-actions";
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ShowServer = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Actions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl overflow-y-auto max-h-screen ">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Server Actions
|
||||
</DialogTitle>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
View all the actions you can do with this server remotely
|
||||
</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-3 w-full gap-1">
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Traefik</CardTitle>
|
||||
<CardDescription>
|
||||
Deploy your new project in one-click.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TraefikActions serverId={serverId} />
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{/* <ShowTraefikSystem serverId={serverId} /> */}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -28,42 +28,14 @@ import { AddServer } from "./add-server";
|
||||
import { SetupServer } from "./setup-server";
|
||||
import { UpdateServer } from "./update-server";
|
||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||
import { ShowModalLogs } from "../web-server/show-modal-logs";
|
||||
import { ToggleTraefikDashboard } from "./toggle-traefik-dashboard";
|
||||
import { EditTraefikEnv } from "../web-server/edit-traefik-env";
|
||||
import { ShowServer } from "./show-server";
|
||||
import { ShowServerActions } from "./actions/show-server-actions";
|
||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||
|
||||
export const ShowServers = () => {
|
||||
const { data, refetch } = api.server.all.useQuery();
|
||||
const { mutateAsync } = api.server.remove.useMutation();
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanDockerBuilder,
|
||||
isLoading: cleanDockerBuilderIsLoading,
|
||||
} = api.settings.cleanDockerBuilder.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanUnusedImages,
|
||||
isLoading: cleanUnusedImagesIsLoading,
|
||||
} = api.settings.cleanUnusedImages.useMutation();
|
||||
|
||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||
api.settings.reloadTraefik.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanUnusedVolumes,
|
||||
isLoading: cleanUnusedVolumesIsLoading,
|
||||
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanStoppedContainers,
|
||||
isLoading: cleanStoppedContainersIsLoading,
|
||||
} = api.settings.cleanStoppedContainers.useMutation();
|
||||
|
||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||
api.settings.cleanAll.useMutation();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-2 flex flex-row justify-between items-end">
|
||||
@@ -149,17 +121,7 @@ export const ShowServers = () => {
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading ||
|
||||
reloadTraefikIsLoading
|
||||
}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
@@ -173,7 +135,7 @@ export const ShowServers = () => {
|
||||
<SetupServer serverId={server.serverId} />
|
||||
|
||||
<UpdateServer serverId={server.serverId} />
|
||||
<ShowServer serverId={server.serverId} />
|
||||
<ShowServerActions serverId={server.serverId} />
|
||||
<DialogAction
|
||||
title={"Delete Server"}
|
||||
description="This will delete the server and all associated data"
|
||||
@@ -201,135 +163,14 @@ export const ShowServers = () => {
|
||||
</DialogAction>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Traefik</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadTraefik({
|
||||
serverId: server.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the traefik");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowModalLogs
|
||||
appName="dokploy-traefik"
|
||||
serverId={server.serverId}
|
||||
>
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
<EditTraefikEnv serverId={server.serverId}>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>Modify Env</span>
|
||||
</DropdownMenuItem>
|
||||
</EditTraefikEnv>
|
||||
<ToggleTraefikDashboard
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuLabel>Storage</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedImages({
|
||||
serverId: server?.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned images");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean images");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused images</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedVolumes({
|
||||
serverId: server?.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned volumes");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean volumes");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused volumes</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanStoppedContainers({
|
||||
serverId: server?.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Stopped containers cleaned");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to clean stopped containers",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean stopped containers</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanDockerBuilder({
|
||||
serverId: server?.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Docker Builder");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to clean Docker Builder",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Docker Builder & System</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanAll({
|
||||
serverId: server?.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned all");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean all");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean all</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
export const ToggleTraefikDashboard = ({ serverId }: Props) => {
|
||||
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||
api.settings.toggleDashboard.useMutation();
|
||||
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||
api.settings.haveTraefikDashboardPortEnabled.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await toggleDashboard({
|
||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
refetchDashboard();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,364 +5,47 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
||||
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
|
||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
||||
import { ShowModalLogs } from "./web-server/show-modal-logs";
|
||||
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
|
||||
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
|
||||
import { TerminalModal } from "./web-server/terminal-modal";
|
||||
import { UpdateServer } from "./web-server/update-server";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
||||
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
|
||||
|
||||
export const WebServer = () => {
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync: reloadServer, isLoading } =
|
||||
api.settings.reloadServer.useMutation();
|
||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||
api.settings.reloadTraefik.useMutation();
|
||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||
api.settings.cleanAll.useMutation();
|
||||
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||
api.settings.toggleDashboard.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanDockerBuilder,
|
||||
isLoading: cleanDockerBuilderIsLoading,
|
||||
} = api.settings.cleanDockerBuilder.useMutation();
|
||||
|
||||
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
|
||||
api.settings.cleanMonitoring.useMutation();
|
||||
const {
|
||||
mutateAsync: cleanUnusedImages,
|
||||
isLoading: cleanUnusedImagesIsLoading,
|
||||
} = api.settings.cleanUnusedImages.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanUnusedVolumes,
|
||||
isLoading: cleanUnusedVolumesIsLoading,
|
||||
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanStoppedContainers,
|
||||
isLoading: cleanStoppedContainersIsLoading,
|
||||
} = api.settings.cleanStoppedContainers.useMutation();
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
export const WebServer = ({ className }: Props) => {
|
||||
const { data } = api.admin.one.useQuery();
|
||||
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
|
||||
const { mutateAsync: updateDockerCleanup } =
|
||||
api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||
api.settings.haveTraefikDashboardPortEnabled.useQuery();
|
||||
|
||||
return (
|
||||
<Card className="rounded-lg w-full bg-transparent">
|
||||
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<CardContent className="flex flex-col gap-4 ">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||
<Button isLoading={isLoading} variant="outline">
|
||||
Server
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadServer()
|
||||
.then(async () => {
|
||||
toast.success("Server Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.success("Server Reloaded");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy">
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
|
||||
<ShowServerTraefikConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View Traefik config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowServerTraefikConfig>
|
||||
|
||||
<ShowServerMiddlewareConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View middlewares config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowServerMiddlewareConfig>
|
||||
|
||||
<TerminalModal serverId={""}>
|
||||
<span>Enter the terminal</span>
|
||||
</TerminalModal>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||
>
|
||||
<Button
|
||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||
variant="outline"
|
||||
>
|
||||
Traefik
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadTraefik()
|
||||
.then(async () => {
|
||||
toast.success("Traefik Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the traefik");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy-traefik">
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
<ShowMainTraefikConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View Traefik config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowMainTraefikConfig>
|
||||
<EditTraefikEnv>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>Modify Env</span>
|
||||
</DropdownMenuItem>
|
||||
</EditTraefikEnv>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await toggleDashboard({
|
||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
refetchDashboard();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
|
||||
Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DockerTerminalModal appName="dokploy-traefik">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<span>Enter the terminal</span>
|
||||
</DropdownMenuItem>
|
||||
</DockerTerminalModal>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isLoading={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
Space
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedImages()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned images");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean images");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused images</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedVolumes()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned volumes");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean volumes");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused volumes</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanStoppedContainers()
|
||||
.then(async () => {
|
||||
toast.success("Stopped containers cleaned");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean stopped containers");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean stopped containers</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanDockerBuilder()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Docker Builder");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Docker Builder");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Docker Builder & System</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanMonitoring()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Monitoring");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Monitoring");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Monitoring </span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanAll()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned all");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean all");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean all</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ShowDokployActions />
|
||||
<ShowTraefikActions />
|
||||
<ShowStorageActions />
|
||||
|
||||
<UpdateServer />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Server IP: {data?.serverIp}
|
||||
Server IP: {data?.host}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
checked={data?.enableDockerCleanup}
|
||||
onCheckedChange={async (e) => {
|
||||
await updateDockerCleanup({
|
||||
enableDockerCleanup: e,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Docker Cleanup Enabled");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Docker Cleanup Error");
|
||||
});
|
||||
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||
</div>
|
||||
<ToggleDockerCleanup />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateMainTraefikConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
});
|
||||
|
||||
type UpdateTraefikConfig = z.infer<typeof UpdateMainTraefikConfigSchema>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
const { data, refetch } = api.settings.readTraefikConfig.useQuery();
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.updateTraefikConfig.useMutation();
|
||||
|
||||
const form = useForm<UpdateTraefikConfig>({
|
||||
defaultValues: {
|
||||
traefikConfig: "",
|
||||
},
|
||||
disabled: canEdit,
|
||||
resolver: zodResolver(UpdateMainTraefikConfigSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateTraefikConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
form.clearErrors("traefikConfig");
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-main-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4 relative"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`providers:
|
||||
docker:
|
||||
defaultRule: 'Host('dokploy.com')'
|
||||
file:
|
||||
directory: /etc/dokploy/traefik
|
||||
watch: true
|
||||
entryPoints:
|
||||
web:
|
||||
address: ':80'
|
||||
websecure:
|
||||
address: ':443'
|
||||
api:
|
||||
insecure: true
|
||||
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit}
|
||||
form="hook-form-update-main-traefik-config"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
});
|
||||
|
||||
type UpdateServerMiddlewareConfig = z.infer<
|
||||
typeof UpdateServerMiddlewareConfigSchema
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
||||
const { data, refetch } = api.settings.readMiddlewareTraefikConfig.useQuery();
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.updateMiddlewareTraefikConfig.useMutation();
|
||||
|
||||
const form = useForm<UpdateServerMiddlewareConfig>({
|
||||
defaultValues: {
|
||||
traefikConfig: "",
|
||||
},
|
||||
disabled: canEdit,
|
||||
resolver: zodResolver(UpdateServerMiddlewareConfigSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
console.log(error);
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
form.clearErrors("traefikConfig");
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Middleware config Updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the middleware traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update Middleware config</DialogTitle>
|
||||
<DialogDescription>Update the middleware config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
rule: Host('domain.com')
|
||||
service: container-name
|
||||
entryPoints:
|
||||
- web
|
||||
tls: false
|
||||
middlewares: []
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit}
|
||||
form="hook-form-update-server-traefik-config"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateServerTraefikConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
});
|
||||
|
||||
type UpdateServerTraefikConfig = z.infer<
|
||||
typeof UpdateServerTraefikConfigSchema
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
const { data, refetch } = api.settings.readWebServerTraefikConfig.useQuery();
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.updateWebServerTraefikConfig.useMutation();
|
||||
|
||||
const form = useForm<UpdateServerTraefikConfig>({
|
||||
defaultValues: {
|
||||
traefikConfig: "",
|
||||
},
|
||||
disabled: canEdit,
|
||||
resolver: zodResolver(UpdateServerTraefikConfigSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateServerTraefikConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
console.log(error);
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
form.clearErrors("traefikConfig");
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
rule: Host('domain.com')
|
||||
service: container-name
|
||||
entryPoints:
|
||||
- web
|
||||
tls: false
|
||||
middlewares: []
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit}
|
||||
form="hook-form-update-server-traefik-config"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
|
||||
export const ShowSupport = () => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="rounded-full">
|
||||
<span className="text-sm font-semibold">Support </span>
|
||||
<HeartIcon className="size-4 text-red-500 fill-red-600 animate-heartbeat " />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl ">
|
||||
<DialogHeader className="text-center flex justify-center items-center">
|
||||
<DialogTitle>Dokploy Support</DialogTitle>
|
||||
<DialogDescription>Consider supporting Dokploy</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid w-full gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-sm font-semibold">Name</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
2
apps/dokploy/drizzle/0038_normal_spiral.sql
Normal file
2
apps/dokploy/drizzle/0038_normal_spiral.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "server" ADD COLUMN "enableDockerCleanup" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "server" DROP COLUMN IF EXISTS "redisPassword";
|
||||
1
apps/dokploy/drizzle/0039_cultured_norrin_radd.sql
Normal file
1
apps/dokploy/drizzle/0039_cultured_norrin_radd.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "server" ALTER COLUMN "enableDockerCleanup" DROP NOT NULL;
|
||||
3823
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3823
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3823
apps/dokploy/drizzle/meta/0039_snapshot.json
Normal file
3823
apps/dokploy/drizzle/meta/0039_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -267,6 +267,20 @@
|
||||
"when": 1726462845274,
|
||||
"tag": "0037_quick_callisto",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "6",
|
||||
"when": 1726962608998,
|
||||
"tag": "0038_normal_spiral",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 39,
|
||||
"version": "6",
|
||||
"when": 1726962717256,
|
||||
"tag": "0039_cultured_norrin_radd",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
one: adminProcedure.query(async () => {
|
||||
one: adminProcedure.query(async ({ ctx }) => {
|
||||
const { sshPrivateKey, ...rest } = await findAdmin();
|
||||
return {
|
||||
haveSSH: !!sshPrivateKey,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
apiReadStatsLogs,
|
||||
apiReadTraefikConfig,
|
||||
apiSaveSSHKey,
|
||||
apiStorage,
|
||||
apiServerSchema,
|
||||
apiTraefikConfig,
|
||||
apiUpdateDockerCleanup,
|
||||
} from "@/server/db/schema";
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
} from "../services/settings";
|
||||
import { canAccessToTraefikFiles } from "../services/user";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { findServerById, updateServerById } from "../services/server";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
@@ -67,7 +68,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reloadTraefik: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
if (input?.serverId) {
|
||||
@@ -94,44 +95,46 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cleanUnusedImages: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedVolumes: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedVolumes(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanStoppedContainers: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanDockerBuilder: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
}),
|
||||
cleanDockerPrune: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
cleanAll: adminProcedure.input(apiStorage).mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
cleanAll: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
return true;
|
||||
}),
|
||||
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||
const { MONITORING_PATH } = paths();
|
||||
await recreateDirectory(MONITORING_PATH);
|
||||
@@ -175,25 +178,43 @@ export const settingsRouter = createTRPCRouter({
|
||||
updateDockerCleanup: adminProcedure
|
||||
.input(apiUpdateDockerCleanup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await updateAdmin(ctx.user.authId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (admin.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
await sendDockerCleanupNotifications();
|
||||
if (input.serverId) {
|
||||
await updateServerById(input.serverId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages(server.serverId);
|
||||
await cleanUpDockerBuilder(server.serverId);
|
||||
await cleanUpSystemPrune(server.serverId);
|
||||
await sendDockerCleanupNotifications();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const currentJob = scheduledJobs["docker-cleanup"];
|
||||
currentJob?.cancel();
|
||||
await updateAdmin(ctx.user.authId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (admin.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
await sendDockerCleanupNotifications();
|
||||
});
|
||||
} else {
|
||||
const currentJob = scheduledJobs["docker-cleanup"];
|
||||
currentJob?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -253,7 +274,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
return getDokployVersion();
|
||||
}),
|
||||
readDirectories: protectedProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
||||
@@ -352,17 +373,19 @@ export const settingsRouter = createTRPCRouter({
|
||||
return openApiDocument;
|
||||
},
|
||||
),
|
||||
readTraefikEnv: adminProcedure.input(apiStorage).query(async ({ input }) => {
|
||||
const command =
|
||||
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
|
||||
readTraefikEnv: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.query(async ({ input }) => {
|
||||
const command =
|
||||
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
|
||||
|
||||
if (input?.serverId) {
|
||||
const result = await execAsyncRemote(input.serverId, command);
|
||||
if (input?.serverId) {
|
||||
const result = await execAsyncRemote(input.serverId, command);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
const result = await execAsync(command);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
const result = await execAsync(command);
|
||||
return result.stdout.trim();
|
||||
}),
|
||||
}),
|
||||
|
||||
writeTraefikEnv: adminProcedure
|
||||
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
||||
@@ -376,7 +399,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
haveTraefikDashboardPortEnabled: adminProcedure
|
||||
.input(apiStorage)
|
||||
.input(apiServerSchema)
|
||||
.query(async ({ input }) => {
|
||||
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;
|
||||
|
||||
|
||||
@@ -105,20 +105,44 @@ echo "$json_output"
|
||||
}
|
||||
const items = readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
return items.map((item) => {
|
||||
const fullPath = join(dirPath, item.name);
|
||||
if (item.isDirectory()) {
|
||||
return {
|
||||
id: fullPath,
|
||||
name: item.name,
|
||||
type: "directory",
|
||||
children: readDirectory(fullPath),
|
||||
};
|
||||
const stack = [dirPath];
|
||||
const result: TreeDataItem[] = [];
|
||||
const parentMap: Record<string, TreeDataItem[]> = {};
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentPath = stack.pop();
|
||||
if (!currentPath) continue;
|
||||
|
||||
const items = readdirSync(currentPath, { withFileTypes: true });
|
||||
const currentDirectoryResult: TreeDataItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = join(currentPath, item.name);
|
||||
if (item.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
const directoryItem: TreeDataItem = {
|
||||
id: fullPath,
|
||||
name: item.name,
|
||||
type: "directory",
|
||||
children: [],
|
||||
};
|
||||
currentDirectoryResult.push(directoryItem);
|
||||
parentMap[fullPath] = directoryItem.children as TreeDataItem[];
|
||||
} else {
|
||||
const fileItem: TreeDataItem = {
|
||||
id: fullPath,
|
||||
name: item.name,
|
||||
type: "file",
|
||||
};
|
||||
currentDirectoryResult.push(fileItem);
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: fullPath,
|
||||
name: item.name,
|
||||
type: "file",
|
||||
};
|
||||
}) as unknown as Promise<TreeDataItem[]>;
|
||||
|
||||
if (parentMap[currentPath]) {
|
||||
parentMap[currentPath].push(...currentDirectoryResult);
|
||||
} else {
|
||||
result.push(...currentDirectoryResult);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -63,7 +63,10 @@ export const apiUpdateDockerCleanup = createSchema
|
||||
.pick({
|
||||
enableDockerCleanup: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTraefikConfig = z.object({
|
||||
traefikConfig: z.string().min(1),
|
||||
@@ -84,7 +87,7 @@ export const apiEnableDashboard = z.object({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiStorage = z
|
||||
export const apiServerSchema = z
|
||||
.object({
|
||||
serverId: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -30,7 +30,7 @@ export const server = pgTable("server", {
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("server")),
|
||||
redisPassword: text("redisPassword").notNull().default("xYBugfHkULig1iLN"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
Reference in New Issue
Block a user